engine cleanup passes 4-7 + multi-instancing ambient rip

Squashes 146 commits from battle-engine-extraction. Net: 2,045 files changed,
+11,896 / -158,687 lines. Ships engine passes 4-7 (dead-code cull, view-layer
stub, receive-path shrink) plus the Phase-5 AsyncLocal ambient deletion that
turns concurrent battles into a type-system property rather than a scope
contract.

## What landed

**Passes 4-7 (chunks 1-34):** Extended the Phase-4 const-false collapse into a
cascading cull across the skill graph, view layer, and receive-path periphery.
Six mode flags (IsWatchBattle/IsReplayBattle/IsAdmin/IsAdminWatch/IsPuzzleQuest/
IsAINetwork) became `const false`, every guarded block deleted. Field*.cs
subclass ctors + BackGroundBase + ObjectChecker culled to no-ops. Mulligan
family reworked to take a mgr param through IMulliganMgr.InitMulligan.
Emotion/Recovery/Resource clusters null-stubbed. Prediction/OperationSimulator/
skill filters converted from static ambient reads to per-mgr reads via
SkillPrm.ownerCard.SelfBattlePlayer.BattleMgr / ins.BattleMgr / this.BattleMgr.

**Phase-5 ambient rip (chunks 35-47):** Deleted BattleAmbient / BattleAmbient-
Context / TestBattleScope in full. Every per-battle mutable slot now lives on
the mgr instance itself:
  mgr.InstanceIsForecast / InstanceIsRandomDraw / InstanceRecoveryInfo /
  InstanceViewerId / InstanceNetworkAgent / GameMgr
BattleManagerBase.GetIns() returns null unconditionally; the residual static
flags + 3 façades (Certification.ViewerId, Data.BattleRecoveryInfo,
ToolboxGame.RealTimeNetworkAgent) are null-tolerant defaults kept for the
handful of engine-internal readers that still reference their types. Zero
BattleAmbient references anywhere in engine + node + tests.

Added pre-seeded GameMgr ctor overload threaded through the mgr chain
(BattleManagerBase → SingleBattleMgr / NetworkBattleManagerBase → NetworkStandard-
BattleMgr → HeadlessBattleMgr / HeadlessNetworkBattleMgr). Fixtures build a
GameMgr, seed it via HeadlessEngineEnv.SeedCharaIds/SeedNetUser, and pass it
to the mgr's ctor — no ambient reach.

Node side (SVSim.BattleNode/SessionBattleEngine): _ctx replaced with a plain
GameMgr field; 34 `using var _ambient = BattleAmbient.Enter(_ctx)` scope wraps
ripped from every accessor and mutator; EngineGlobalInit.WirePerSessionGameMgr
takes GameMgr as a param and runs from SessionBattleEngine.SetupInternal
BEFORE mgr construction.

Test side: TestBattleScope deleted; 18 fixture [SetUp]s migrated to
`HeadlessEngineEnv.EnsureProcessGlobals()`; MultiInstanceEngineTests rewritten
around per-mgr construction (GetIns() → null is the pinned invariant).

## Regression fixes

- **chunk-48** (MulliganCtrl): chunk-35's `= null` stubs on card lookups broke
  the live receive-driven Deal path (BattlePlayerBase.DrawCard NRE'd downstream
  of NetworkPlayerMulliganCtrl.StartMulliganVfx). Restored the three lookups
  via `_battlePlayer.BattleMgr.GetBattleCardIdx`. Engine tests were satisfied
  by the WireMulliganPhase seam; unit tests exposed the live-path gap.

## Ship state

- SVSim.BattleEngine.Tests: 56/56 pass, 2 skip
- SVSim.UnitTests: 1554/1554 pass (was 1523/31-fail before chunk 48)
- Solution build: 0 source warnings (40 pre-existing NU1902 MessagePack CVEs
  in SVSim.EmulatedEntrypoint, unrelated)
- Sequential PVP smoke: verified live (two back-to-back battles, no regression
  on cleanup/spinup)
- Concurrent PVP smoke: verified live

Adds tools/engine-port/ClosureAnalyzer/ — the Roslyn transitive-type-closure
analyzer needed to make future cascade cleanup safe (per feedback memory
"Engine cleanup needs closure tool" from the 2026-06-28 pass-3 failure).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-03 19:18:54 -04:00
parent 5c1db83967
commit 2d9a6eea4b
2045 changed files with 11704 additions and 158495 deletions

View File

@@ -60,11 +60,9 @@ internal static class EngineGlobalInit
public static void EnsureInitialized()
{
// GameMgr is now per-session (BattleAmbientContext.GameMgr), so its DataMgr/NetworkUserInfoData
// wiring must run on EVERY call — once-per-process gating it (under _done) leaves a second-or-
// later session with an unwired ctx.GameMgr and NREs in NetworkBattleManagerBase.CreateBackgroundId.
// The wiring itself is idempotent (zero-or-null guards), so re-running is safe.
WirePerSessionGameMgr();
// Phase-5 chunk 46: per-session GameMgr wiring moved to SessionBattleEngine.SetupInternal
// where the session's own GameMgr is in scope. EnsureInitialized only handles the true
// process-globals below.
if (_done) return;
lock (_gate)
@@ -86,14 +84,15 @@ internal static class EngineGlobalInit
.SetValue(null, new Crossover());
}
// Suppress VFX / take the virtual-battle resolution path (no live view layer).
BattleManagerBase.IsForecast = true;
// Phase-5 chunk 43: was `BattleManagerBase.IsForecast = true` — a static ambient-bridge
// write that ran before any mgr was constructed, so it stashed in ambient.IsForecast for
// pre-Phase-5 propagation onto the mgr's InstanceIsForecast at attach. With chunk 42's
// ambient rip that propagation is gone and the write became a silent no-op. Init here is
// now redundant: BattleManagerBase.InstanceIsForecast = true by default.
// Post-Task-8: BattleRecoveryInfo lives on the per-session BattleAmbientContext (RecoveryInfo,
// pre-seeded with an uninitialized no-op instance in SessionBattleEngine's field initializer).
// Each session owns its own (IsMulliganEnd=false default), so the MulliganMgrBase.StartDeal
// read resolves per-session — the historical process-global Data.BattleRecoveryInfo write that
// lived here was dead the moment the ambient seam landed (reviewer-flagged in Task 7).
// BattleRecoveryInfo lives on the mgr instance itself (InstanceRecoveryInfo). Historical
// process-global write here was dead the moment the mgr's per-instance slot landed
// (Task 7 / Phase-5a).
// --- static CardMaster (full cards.json) ----------------------------------------------
// ALWAYS rebuild + re-inject the FULL master. We must not defer to a possibly-thin
@@ -121,46 +120,31 @@ internal static class EngineGlobalInit
udidField.SetValue(null, "headless-udid");
// --- Cute.Certification.viewer_id ------------------------------------------------------
// Post-Task-8: viewer id lives on the per-session BattleAmbientContext (ViewerId, init-only).
// SessionBattleEngine seeds it from ThisViewerId in its field initializer and enters the
// ambient on every Setup/Receive — so the engine's Certification.ViewerId getter
// (BattleAmbient.Require().ViewerId) always resolves cleanly when the engine code is
// reached. The reflection write that used to seed `Certification._viewerIdFallback` is now
// dead (the static is deleted). Left as a comment-only marker so the design intent (this
// viewer id defines the engine's "player" perspective for the IsRecovery target parse)
// stays discoverable from the global init path.
// Viewer id lives on the mgr instance (InstanceViewerId, default 1001 = ThisViewerId).
// Certification.ViewerId's static getter reads the mgr via GetIns()?.InstanceViewerId
// — no seeding needed here.
_done = true;
}
}
// Per-session GameMgr wiring: under the ambient seam, GameMgr.GetIns() resolves to the SESSION's
// BattleAmbientContext.GameMgr — a fresh instance per SessionBattleEngine. So the DataMgr chara ids
// and NetworkUserInfoData seeding must run on every Setup, not just process-once. The reflection
// helpers below are idempotent (SetFieldIfZeroOrNull is a no-op once set; netUser is null-guarded).
private static void WirePerSessionGameMgr()
// Per-session GameMgr wiring: seeds DataMgr chara ids + NetworkUserInfoData on the session's own
// GameMgr. Caller (SessionBattleEngine.SetupInternal) passes _gameMgr BEFORE mgr construction so
// the base ctor's BattlePlayer/DataMgr reads resolve. Idempotent (SetFieldIfZeroOrNull no-ops once
// set; netUser is null-guarded).
public static void WirePerSessionGameMgr(GameMgr gm)
{
// --- GameMgr DataMgr leader chara ids --------------------------------------------------
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/
// AvatarBattle info (more null statics) the resolution path doesn't need. Idempotent
// (plain assignment); only meaningful when still 0.
var dm = GameMgr.GetIns().GetDataMgr();
var dm = gm.GetDataMgr();
SetFieldIfZeroOrNull(dm, "_playerCharaId", PlayerCharaId);
SetFieldIfZeroOrNull(dm, "_enemyCharaId", EnemyCharaId);
// --- NetworkUserInfoData (background lookup on the network mgr's CreateBackgroundId) ----
// NetworkBattleManagerBase.CreateBackgroundId reads
// GameMgr.GetIns().GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no
// bg id. GameMgr leaves _netUser null with no lazy init; seed a no-op instance whose
// _selfInfo carries just fieldId=1 (== ForestField, a valid background). Only seed when
// absent so a HeadlessEngineEnv-set instance is preserved.
if (GameMgr.GetIns().GetNetworkUserInfoData() == null)
if (gm.GetNetworkUserInfoData() == null)
{
var netUser = new NetworkUserInfoData();
netUser.SetSelfInfo(
new Dictionary<string, object> { ["fieldId"] = 1 },
isWatchReplayRecovery: false);
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
gm.SetNetworkUserInfoData(netUser);
}
}