test(engine-ambient): TestBattleScope + HeadlessFixture split for multi-instance
Step 6 of multi-instancing migration. HeadlessEngineEnv.EnsureInitialized is split into EnsureProcessGlobals (idempotent, process-once) + SeedCharaIdsOnCurrentAmbient (per-test). New TestBattleScope IDisposable sets up a fresh BattleAmbientContext per test. NonParallelizable removed from converted classes; assembly-level Parallelizable(Fixtures) enabled. SVSim.BattleEngine.Tests fully green under parallel test execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,10 +172,26 @@ namespace SVSim.BattleEngine.Tests
|
||||
public const int RngDeckCardC = SummonedTokenId; // neutral 2/2 -> Index-order position 2
|
||||
|
||||
private static bool _done;
|
||||
private static readonly object _processGlobalsGate = new object();
|
||||
|
||||
public static void EnsureInitialized()
|
||||
// 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).
|
||||
public static void EnsureProcessGlobals()
|
||||
{
|
||||
if (_done) return;
|
||||
lock (_processGlobalsGate)
|
||||
{
|
||||
if (_done) return;
|
||||
EnsureProcessGlobalsCore();
|
||||
_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureProcessGlobalsCore()
|
||||
{
|
||||
// Wizard.Data.Load: static /load/index snapshot. The ctor's CreateBackgroundId reads
|
||||
// Data.Load.data._userTutorial (LoadDetail self-inits _userTutorial). Suppress VFX too.
|
||||
Wizard.Data.Load = new Load { data = new LoadDetail() };
|
||||
@@ -184,7 +200,6 @@ namespace SVSim.BattleEngine.Tests
|
||||
typeof(Wizard.Data).GetProperty("Crossover",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
|
||||
.SetValue(null, new Wizard.Crossover());
|
||||
BattleManagerBase.IsForecast = true;
|
||||
// CardMaster must be non-null before construction (the leader/class card looks up id 0).
|
||||
// Load the M2 vanilla follower + the M3 fixed-damage spell + the M4 self-buff follower +
|
||||
// the M5 summon-token spell AND the token it summons so each oracle can create + look up
|
||||
@@ -195,6 +210,22 @@ namespace SVSim.BattleEngine.Tests
|
||||
DynamicDamageSpellId);
|
||||
// Master reference data (class-character list) for leader/class card resolution.
|
||||
HeadlessMasterData.Install();
|
||||
|
||||
// The network emit path's payload builder (RealTimeNetworkAgent.CreateEmitData) reads
|
||||
// Cute.Certification.Udid (RealTimeNetworkAgent.cs:1407). The Udid getter lazily decodes from
|
||||
// Toolbox.SavedataManager (Certification.cs:35), which is null headless. Seed the private static
|
||||
// backing field with a non-empty placeholder so the getter short-circuits before touching the
|
||||
// savedata manager. The value is opaque to the engine (it's just echoed into the emit dict).
|
||||
typeof(Cute.Certification)
|
||||
.GetField("udid", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
|
||||
.SetValue(null, "headless-udid");
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
// 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
|
||||
@@ -202,6 +233,13 @@ namespace SVSim.BattleEngine.Tests
|
||||
var dm = GameMgr.GetIns().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()
|
||||
{
|
||||
// NetworkBattleManagerBase.CreateBackgroundId() (M13) reads
|
||||
// GameMgr.GetIns().GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no
|
||||
// bg id (NullRecoveryManager.BackGroundId == -1). In production RealTimeNetworkAgent seeds
|
||||
@@ -216,17 +254,6 @@ namespace SVSim.BattleEngine.Tests
|
||||
new System.Collections.Generic.Dictionary<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
|
||||
// The network emit path's payload builder (RealTimeNetworkAgent.CreateEmitData) reads
|
||||
// Cute.Certification.Udid (RealTimeNetworkAgent.cs:1407). The Udid getter lazily decodes from
|
||||
// Toolbox.SavedataManager (Certification.cs:35), which is null headless. Seed the private static
|
||||
// backing field with a non-empty placeholder so the getter short-circuits before touching the
|
||||
// savedata manager. The value is opaque to the engine (it's just echoed into the emit dict).
|
||||
typeof(Cute.Certification)
|
||||
.GetField("udid", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
|
||||
.SetValue(null, "headless-udid");
|
||||
|
||||
_done = true;
|
||||
}
|
||||
|
||||
// Seed each leader's starting life on a freshly-constructed mgr. The engine does this in
|
||||
@@ -336,7 +363,7 @@ namespace SVSim.BattleEngine.Tests
|
||||
// Returns the constructed HeadlessBattleMgr; the caller seeds hands/decks/boards and plays.
|
||||
public static HeadlessBattleMgr NewAuthoritativeBattle(IRandomSource rng)
|
||||
{
|
||||
EnsureInitialized(); // sets IsForecast = true among other globals
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
BattleManagerBase.IsRandomDraw = true; // the second RNG gate (F-RNG-2)
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
@@ -361,7 +388,7 @@ namespace SVSim.BattleEngine.Tests
|
||||
public static (HeadlessNetworkBattleMgr mgr, System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI> emitted)
|
||||
NewNetworkEmitBattle(IRandomSource rng = null)
|
||||
{
|
||||
EnsureInitialized(); // sets IsForecast = true among other globals
|
||||
EnsureProcessGlobals(); // sets IsForecast = true among other globals
|
||||
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
// NOTE: IsRecovery is left FALSE here (unlike the solo NewAuthoritativeBattle). The network
|
||||
// emit path is gated on !IsRecovery in BOTH places: NetworkStandardBattleMgr.SendPlayCard
|
||||
|
||||
Reference in New Issue
Block a user