diff --git a/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs b/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs
index 4233c67..bad88f1 100644
--- a/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs
+++ b/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs
@@ -1,6 +1,7 @@
using NUnit.Framework;
using SVSim.BattleNode.Sessions.Engine;
using System.Linq;
+using SVSim.BattleEngine.Tests;
namespace SVSim.BattleEngine.Tests.SessionEngine;
@@ -15,6 +16,10 @@ public class SessionEngineSpellboostTests
var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
+ // Belt-and-suspenders (matches the sibling tests): load the decks into the harness master so
+ // this test never depends on global card-master contents. EnsureInitialized() above still
+ // proves EngineGlobalInit's own path works.
+ foreach (var id in deckA.Concat(deckB).Distinct()) HeadlessCardMaster.Load((int)id);
var engine = new SessionBattleEngine();
Assert.DoesNotThrow(() => engine.Setup(masterSeed: 12345, seatADeck: deckA, seatBDeck: deckB));
Assert.That(engine.IsReady, Is.True, "engine must be ready after EngineGlobalInit (carried-risk fix)");
diff --git a/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs b/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs
index 6f386c8..b9b08ec 100644
--- a/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs
+++ b/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs
@@ -34,8 +34,11 @@ namespace SVSim.BattleNode.Sessions.Engine;
/// HeadlessCardMaster + HeadlessMasterData; the reflection seams are transcribed verbatim.
/// It differs in exactly three ways: (1) it loads the FULL cards.json (every row, no id filter) since
/// the live host serves arbitrary decks; (2) it installs ALL 8 classes in ClassCharacterList; and
-/// (3) every set is guarded so the call is idempotent AND does not fight the test
-/// HeadlessEngineEnv if both run in one NUnit process.
+/// (3) the call is idempotent (process-once via _done). Coexistence: it does not OVERWRITE
+/// harmless globals another initializer set (Data.Load, Crossover, leader chara ids,
+/// netUser, udid are all set-only-if-absent), but it ALWAYS guarantees the postcondition — the FULL
+/// card master and the all-8-class Master — by rebuilding/re-injecting both unconditionally rather
+/// than deferring to a possibly-thinner existing global (e.g. a test harness's single-card master).
internal static class EngineGlobalInit
{
private static readonly object _gate = new();
@@ -74,16 +77,19 @@ internal static class EngineGlobalInit
BattleManagerBase.IsForecast = true;
// --- static CardMaster (full cards.json) ----------------------------------------------
- // Skip the (expensive) full load if a CardMaster is already registered as Default — a
- // prior EngineGlobalInit call OR a HeadlessEngineEnv/HeadlessCardMaster load in the same
- // NUnit process already populated it.
- if (!IsCardMasterPopulated())
- LoadFullCardMaster();
+ // ALWAYS rebuild + re-inject the FULL master. We must not defer to a possibly-thin
+ // existing Default (e.g. a HeadlessCardMaster.Load(singleCard) from an earlier test in
+ // the same NUnit process): the postcondition is the COMPLETE card master, and a thin
+ // master makes Setup→SeedDeck NRE in SkillCreator.CreateBuildInfo. EnsureInitialized is
+ // process-once via _done, so an unconditional load runs at most once — the skip-
+ // optimization bought nothing and traded correctness for a coexistence hazard.
+ // LoadFullCardMaster rebuilds + re-injects Default, so it's idempotent.
+ LoadFullCardMaster();
// --- Master reference data (all 8 classes' chara list) ---------------------------------
- // Skip if Data.Master is already non-null with a non-empty ClassCharacterList.
- if (!IsMasterPopulated())
- InstallMaster();
+ // ALWAYS install all 8 classes. Don't defer to a prior (e.g. 2-class) harness master:
+ // the postcondition is all-8-class ClassCharacterList. Idempotent (replaces Data.Master).
+ InstallMaster();
// --- GameMgr DataMgr leader chara ids --------------------------------------------------
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/
@@ -111,11 +117,12 @@ internal static class EngineGlobalInit
// --- Cute.Certification.udid -----------------------------------------------------------
// The emit-path payload builder reads Certification.Udid, whose getter lazily decodes from
// Toolbox.SavedataManager (null headless). Seed the private static backing field with a
- // non-empty placeholder so the getter short-circuits. Only set when empty (coexistence).
+ // non-empty placeholder (opaque — only echoed, never matched) so the getter short-
+ // circuits. Matches the harness value. Only set when empty (coexistence).
var udidField = typeof(Certification).GetField("udid",
BindingFlags.Static | BindingFlags.NonPublic)!;
if (string.IsNullOrEmpty(udidField.GetValue(null) as string))
- udidField.SetValue(null, "host-udid");
+ udidField.SetValue(null, "headless-udid");
_done = true;
}
@@ -123,16 +130,6 @@ internal static class EngineGlobalInit
// --- CardMaster (full load) ----------------------------------------------------------------------
- private static bool IsCardMasterPopulated()
- {
- var idType = typeof(CardMaster).GetNestedType("CardMasterId")!;
- var defaultId = Enum.Parse(idType, "Default");
- var fld = typeof(CardMaster).GetField("_dictCardMaster",
- BindingFlags.Static | BindingFlags.NonPublic)!;
- if (fld.GetValue(null) is not IDictionary dict) return false;
- return dict.Contains(defaultId) && dict[defaultId] != null;
- }
-
// Production difference (1): enumerate EVERY card row — no want.Contains(id) filter.
private static void LoadFullCardMaster()
{
@@ -201,14 +198,6 @@ internal static class EngineGlobalInit
// --- Master reference data (all 8 classes) -------------------------------------------------------
- private static bool IsMasterPopulated()
- {
- if (Data.Master is not Master m) return false;
- var p = typeof(Master).GetProperty("ClassCharacterList",
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- return p?.GetValue(m) is ICollection { Count: > 0 };
- }
-
// Transcribed from HeadlessMasterData.Install. Production difference (2): install ALL 8 classes.
private static void InstallMaster()
{