fix(battlenode): EngineGlobalInit guarantees full-master postcondition (Phase 2 N2 review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,11 @@ namespace SVSim.BattleNode.Sessions.Engine;
|
||||
/// <c>HeadlessCardMaster</c> + <c>HeadlessMasterData</c>; 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
|
||||
/// <c>HeadlessEngineEnv</c> if both run in one NUnit process.</summary>
|
||||
/// (3) the call is idempotent (process-once via <c>_done</c>). Coexistence: it does not OVERWRITE
|
||||
/// harmless globals another initializer set (<c>Data.Load</c>, <c>Crossover</c>, 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).</summary>
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user