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:
@@ -175,10 +175,11 @@ namespace SVSim.BattleEngine.Tests
|
||||
private static readonly object _processGlobalsGate = new object();
|
||||
|
||||
// Process-globals only: load card master, install master data, seed LoadDetail/Crossover,
|
||||
// seed Certification.udid. Per-battle/per-test state (IsForecast, chara ids on the DataMgr,
|
||||
// NetworkUserInfoData) is now seeded inside TestBattleScope's ctor against the per-scope
|
||||
// GameMgr — calling it here would crash because GameMgr.GetIns() Requires an ambient scope.
|
||||
// Thread-safe (assembly-level Parallelizable(Fixtures) means many fixtures' [SetUp] race here).
|
||||
// seed Certification.udid. Per-battle/per-test state (chara ids on the DataMgr,
|
||||
// NetworkUserInfoData) is seeded on the mgr's own GameMgr via NewSeededSingleBattleMgr /
|
||||
// NewSeededHeadlessBattleMgr / NewSeededHeadlessNetworkBattleMgr in the fixture entry points
|
||||
// — no shared state to race here. Thread-safe (assembly-level Parallelizable(Fixtures) means
|
||||
// many fixtures' [SetUp] race this one function).
|
||||
public static void EnsureProcessGlobals()
|
||||
{
|
||||
if (_done) return;
|
||||
@@ -233,39 +234,57 @@ namespace SVSim.BattleEngine.Tests
|
||||
return deck;
|
||||
}
|
||||
|
||||
// Per-ambient seeder: writes the player/enemy chara ids onto the AMBIENT GameMgr's DataMgr.
|
||||
// Called by TestBattleScope after the scope is entered so GameMgr.GetIns() routes to the
|
||||
// per-test GameMgr, not whichever one happened to be ambient last.
|
||||
public static void SeedCharaIdsOnCurrentAmbient()
|
||||
// Per-GameMgr chara-id seeder. Fixtures construct a GameMgr up-front and pass it to the
|
||||
// mgr ctor (chunk-45 overload); this stamps the player/enemy leader chara ids on its DataMgr.
|
||||
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation /
|
||||
// AvatarBattle info (more null statics) the resolution path doesn't need.
|
||||
public static void SeedCharaIds(GameMgr gm)
|
||||
{
|
||||
// Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master).
|
||||
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/
|
||||
// AvatarBattle info (more null statics) which the resolution path doesn't need (the
|
||||
// TryGet* accessors are null-tolerant).
|
||||
var dm = GameMgr.GetIns().GetDataMgr();
|
||||
var dm = gm.GetDataMgr();
|
||||
SetField(dm, "_playerCharaId", HeadlessMasterData.PlayerCharaId);
|
||||
SetField(dm, "_enemyCharaId", HeadlessMasterData.EnemyCharaId);
|
||||
}
|
||||
|
||||
// Per-ambient seeder: installs a no-op NetworkUserInfoData on the AMBIENT GameMgr so
|
||||
// NetworkBattleManagerBase.CreateBackgroundId()'s GetNetworkUserInfoData().GetFieldId() call
|
||||
// resolves (M13). Field id 1 == ForestField, a valid background.
|
||||
public static void SeedNetUserOnCurrentAmbient()
|
||||
// Per-GameMgr net-user seeder. NetworkBattleManagerBase.CreateBackgroundId reads
|
||||
// gm.GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no bg id; the bare
|
||||
// ctor path leaves _netUser null (no lazy init). Seed a no-op instance whose _selfInfo carries
|
||||
// fieldId=1 (== ForestField, valid). Only satisfies the bg lookup — no game-state effect.
|
||||
public static void SeedNetUser(GameMgr gm)
|
||||
{
|
||||
// NetworkBattleManagerBase.CreateBackgroundId() (M13) reads
|
||||
// GameMgr.GetIns().GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no
|
||||
// bg id (NullRecoveryManager.BackGroundId == -1). In production RealTimeNetworkAgent seeds
|
||||
// this NetworkUserInfoData at match start; the bare construction path leaves GameMgr's
|
||||
// _netUser null (no lazy init, unlike the other GodObject getters). Seed a no-op instance
|
||||
// whose _selfInfo carries just "fieldId" (GetFieldId reads _selfInfo["fieldId"]); field id 1
|
||||
// == ForestField, a valid background. Nothing here drives game state — it only satisfies the
|
||||
// network mgr's background lookup, a background lookup the single-battle path
|
||||
// (`SingleBattleMgr`) never performs.
|
||||
var netUser = new NetworkUserInfoData();
|
||||
netUser.SetSelfInfo(
|
||||
new System.Collections.Generic.Dictionary<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
gm.SetNetworkUserInfoData(netUser);
|
||||
}
|
||||
|
||||
// Phase-5 chunk 46: canonical seeded SingleBattleMgr factory. Replaces the historical
|
||||
// `new SingleBattleMgr(new HeadlessContentsCreator())` pattern from every oracle test,
|
||||
// which relied on the ambient bridge to reach a chara-id/net-user-seeded GameMgr. Now the
|
||||
// GameMgr is built + seeded here and passed to the mgr's chunk-45 ctor overload directly.
|
||||
public static SingleBattleMgr NewSeededSingleBattleMgr()
|
||||
{
|
||||
var gm = new GameMgr();
|
||||
SeedCharaIds(gm);
|
||||
SeedNetUser(gm);
|
||||
return new SingleBattleMgr(new HeadlessContentsCreator(), gm);
|
||||
}
|
||||
|
||||
// Same idea for the RNG-injectable HeadlessBattleMgr / HeadlessNetworkBattleMgr twins.
|
||||
public static HeadlessBattleMgr NewSeededHeadlessBattleMgr(IRandomSource rng = null)
|
||||
{
|
||||
var gm = new GameMgr();
|
||||
SeedCharaIds(gm);
|
||||
SeedNetUser(gm);
|
||||
return new HeadlessBattleMgr(new HeadlessContentsCreator(), rng, gm);
|
||||
}
|
||||
|
||||
public static HeadlessNetworkBattleMgr NewSeededHeadlessNetworkBattleMgr(IRandomSource rng = null)
|
||||
{
|
||||
var gm = new GameMgr();
|
||||
SeedCharaIds(gm);
|
||||
SeedNetUser(gm);
|
||||
return new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng, gm);
|
||||
}
|
||||
|
||||
// Seed each leader's starting life on a freshly-constructed mgr. The engine does this in
|
||||
@@ -376,8 +395,16 @@ namespace SVSim.BattleEngine.Tests
|
||||
public static HeadlessBattleMgr NewAuthoritativeBattle(IRandomSource rng)
|
||||
{
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
BattleManagerBase.IsRandomDraw = true; // the second RNG gate (F-RNG-2)
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
// Phase-5 chunk 45: build a pre-seeded GameMgr and hand it to the mgr ctor, bypassing
|
||||
// the ambient bridge. The base ctor's BattlePlayer construction reads chara-id/net-user
|
||||
// through GameMgr.GetIns() (which routes to mgr.GameMgr once base ctor runs), so the
|
||||
// GameMgr must be seeded BEFORE the mgr ctor completes.
|
||||
var gm = new GameMgr();
|
||||
SeedCharaIds(gm);
|
||||
SeedNetUser(gm);
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng, gm);
|
||||
// Phase-5 chunk 42: write InstanceIsRandomDraw directly (mgr not yet ambient-attached).
|
||||
mgr.InstanceIsRandomDraw = true; // the second RNG gate (F-RNG-2)
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
@@ -401,7 +428,11 @@ namespace SVSim.BattleEngine.Tests
|
||||
NewNetworkEmitBattle(IRandomSource rng = null)
|
||||
{
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
// Phase-5 chunk 45: build + seed a per-mgr GameMgr up-front (no ambient bridge).
|
||||
var gm = new GameMgr();
|
||||
SeedCharaIds(gm);
|
||||
SeedNetUser(gm);
|
||||
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng, gm);
|
||||
// NOTE: IsRecovery is left FALSE here (unlike the solo NewAuthoritativeBattle). The network
|
||||
// emit path is gated on !IsRecovery in BOTH places: NetworkStandardBattleMgr.SendPlayCard
|
||||
// (NetworkStandardBattleMgr.cs:155) and the OnSetCardComplete->SendPlayCard subscription in
|
||||
@@ -419,7 +450,10 @@ namespace SVSim.BattleEngine.Tests
|
||||
// play exercises the real view layer — those view touches are satisfied by the no-op view shims
|
||||
// (InitCardTemplates, the HandView/DetailPanel fills below). M3's damage is literal, immune to
|
||||
// any play-count bump the OperateMgr path adds vs the direct path.
|
||||
BattleManagerBase.IsForecast = false;
|
||||
// Phase-5 chunk 42: mgr isn't yet attached to the ambient at this point (_scope.Ctx.Mgr
|
||||
// is set by the caller after this fixture returns), so BattleManagerBase.IsForecast=false
|
||||
// would silently no-op (ambient bridge is gone). Write InstanceIsForecast directly.
|
||||
mgr.InstanceIsForecast = false;
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
SetField(player, "_opponentBattlePlayer", enemy);
|
||||
@@ -447,28 +481,17 @@ namespace SVSim.BattleEngine.Tests
|
||||
// public setter runs cleanly headless. Prepared (50) is the real enum member (RealTimeNetworkAgent.cs:35).
|
||||
agent.SetCurrentMatchingStatus(RealTimeNetworkAgent.MatchingStatus.Prepared);
|
||||
|
||||
// EmitMsgPack -> AddActionSequence (RealTimeNetworkAgent.cs:1773, fired for the PlayActions URI)
|
||||
// does `_gungnir._actionSequenceNum++` and `NetworkLogger.LogInfo(...)`. On the
|
||||
// GetUninitializedObject agent both are null (the real ctor builds them at :289/:301). Seed an
|
||||
// uninitialized Gungnir (its ctor news a ConnectionReporter + Ticks — unneeded; AddActionSequence
|
||||
// only touches the int counter) and the engine's own NetworkNullLogger no-op so the action-seq
|
||||
// bookkeeping runs without crashing. Neither drives game state.
|
||||
SetField(agent, "_gungnir",
|
||||
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Gungnir)));
|
||||
// The engine RTA is a pass-7 stub with no-op method bodies. AddActionSequence /
|
||||
// EmitMsgPack / OnEmit-plumbing were dropped; the historical Gungnir + _notEmit seeds
|
||||
// are no longer needed. NetworkLogger stays as a NetworkNullLogger since it's a
|
||||
// typed property (INetworkLogger<NetworkLog>) that some tests may still inspect.
|
||||
SetProperty(agent, "NetworkLogger", new NetworkNullLogger());
|
||||
|
||||
// Suppress the actual socket transmission. After OnEmit fires (RealTimeNetworkAgent.cs:1270, the
|
||||
// O1 liveness signal), EmitMsgPack -> EmitMsgUriPack reaches the stockEmitMessageMgr / _manager.Socket
|
||||
// network I/O (RealTimeNetworkAgent.cs:1444+/1487) — none of which exists headless. The engine's
|
||||
// OWN _notEmit flag (set in recovery/replay) short-circuits EmitMsgUriPack at :1438 BEFORE any of
|
||||
// that, so the emit stays genuine (OnEmit already fired through the real send path) while the
|
||||
// byte-push is skipped. This is the only honest way to terminate the path headless: we are NOT
|
||||
// faking OnEmit, only declining to open a socket we cannot open.
|
||||
SetField(agent, "_notEmit", true);
|
||||
|
||||
var emitted = new System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI>();
|
||||
agent.OnEmit += uri => emitted.Add(uri);
|
||||
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
|
||||
// Phase-5 chunk 40: ambient fallback removed from ToolboxGame.SetRealTimeNetworkBattle;
|
||||
// fixture has mgr in scope, so seed it directly instead of via the ambient-routed static.
|
||||
mgr.InstanceNetworkAgent = agent;
|
||||
|
||||
return (mgr, emitted);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user