Hygiene B0-B1: correctness & latent traps

- Restore swallowed [Test] on EnemyAIMathTests.SlideVelocity_DegenerateNormal_DeflectsToTangent (dead regression guard for the shipped enemy-stuck-on-cover fix; now runs + passes).
- Extract shared HarvestMath.DepositYield used by ResourceHarvestSystem + MeleeComboSystem; fixes melee mining silently ignoring per-item StackMax (it hard-coded DefaultStackMax) and hoists the melee ledger-buffer fetch out of the per-target loop.
- StatMath.Apply switches on the raw byte (case (byte)ModOp.X) instead of casting to the enum inside the Bursted fold — removes the latent cross-assembly enum-in-Burst ICE trap.
- Add [Min] guards on CycleDirectorAuthoring loss-critical ints (CoreIntegrityMax>=1 so a mis-authored 0 can't bake an instant-loss core; siege sizes >=0).

456/456 EditMode tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:53:32 -07:00
parent 7b64c1caa3
commit b1041003f0
7 changed files with 80 additions and 47 deletions
@@ -149,28 +149,12 @@ namespace ProjectM.Server
// Route the yield into the HARVESTING player's PERSONAL inventory. The projectile carries the
// firing player's GhostOwner (AbilityFireSystem); the owner is read OPTIONALLY (cached lookup) so
// an un-owned projectile (or a test projectile with no GhostOwner) falls through to the ledger.
int remainder = amount;
// Base-region nodes credit the SHARED ledger DIRECTLY (the build currency pool); expedition / un-tagged
// nodes keep the personal-inventory reroute (spill-to-ledger). Untagged -> not Base -> inventory path.
if (!tgtToLedger[bestIdx]
&& m_GhostOwnerLookup.HasComponent(projEntity)
&& playerByConn.TryGetValue(m_GhostOwnerLookup[projEntity].NetworkId, out var player)
&& m_InvLookup.HasBuffer(player))
{
int stackMax = Tuning.DefaultStackMax;
if (haveDb && itemDb.Value.IsCreated)
{
ref var itemBlob = ref itemDb.Value.Value;
if (itemBlob.TryGetItem(yieldId, out var def) && def.StackMax > 0)
stackMax = def.StackMax;
}
var inv = m_InvLookup[player];
remainder = InventoryMath.Deposit(inv, yieldId, amount, stackMax, Tuning.InventoryMaxSlots);
}
// Unresolvable owner or a full bag: the remainder credits the shared ledger (no-loss valve).
if (remainder > 0)
StorageMath.Deposit(ledger, yieldId, remainder);
Entity harvester = Entity.Null;
if (m_GhostOwnerLookup.HasComponent(projEntity)
&& playerByConn.TryGetValue(m_GhostOwnerLookup[projEntity].NetworkId, out var ownedPlayer))
harvester = ownedPlayer;
HarvestMath.DepositYield(yieldId, amount, tgtToLedger[bestIdx], harvester,
m_InvLookup, ledger, true, haveDb, itemDb);
int rem = tgtRemaining[bestIdx] - amount;
tgtRemaining[bestIdx] = rem;
ecb.DestroyEntity(projEntity);