extern alias engine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using BattleManagerBase = engine::BattleManagerBase;
using BattleRecoveryInfo = engine::Wizard.BattleRecoveryInfo;
using CardCSVData = engine::Wizard.CardCSVData;
using CardMaster = engine::Wizard.CardMaster;
using Certification = engine::Cute.Certification;
using ClassCharacterMasterData = engine::Wizard.ClassCharacterMasterData;
using Crossover = engine::Wizard.Crossover;
using Data = engine::Wizard.Data;
using GameMgr = engine::GameMgr;
using Load = engine::Load;
using LoadDetail = engine::LoadDetail;
using Master = engine::Wizard.Master;
using NetworkUserInfoData = engine::NetworkUserInfoData;
namespace SVSim.BattleNode.Sessions.Engine;
/// Host-owned, process-once initializer for the engine's global statics (Phase 2 N2,
/// carried-risk A). The decompiled engine assumes a set of process-globals exist that the client
/// populates from /load/index at login: the static CardMaster, Wizard.Data
/// (Load/Master/Crossover), the GameMgr DataMgr chara ids, a NetworkUserInfoData, and
/// Cute.Certification.udid. Without them throws inside
/// its try/catch and the shadow silently no-ops (the N1 carried risk). Calling
/// once at host startup primes them so Setup succeeds.
///
/// This is the production analogue of the test fixtures HeadlessEngineEnv.EnsureInitialized +
/// 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) 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();
private static bool _done;
private static readonly string CardsJsonPath =
Path.Combine(AppContext.BaseDirectory, "Data", "cards.json");
// chara ids -> a ClassCharacterMasterData in Master; mirrors HeadlessMasterData.
private const int PlayerCharaId = 1;
private const int EnemyCharaId = 2;
/// The headless engine's "self" viewer id (seeded into Certification.viewer_id). Any
/// stable nonzero value works; it only has to be DISTINCT from the vid an attack/evolve stamps for a
/// target on the OTHER seat so the IsRecovery target parse resolves owners correctly. Exposed for the
/// node-native harness to build attack frames whose target vid matches this perspective.
internal const int ThisViewerId = 1001;
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();
if (_done) return;
lock (_gate)
{
if (_done) return;
// --- Wizard.Data globals (the static /load/index snapshot) -----------------------------
// The mgr ctor's CreateBackgroundId reads Data.Load.data._userTutorial (LoadDetail
// self-inits _userTutorial). ??= so we don't clobber a snapshot HeadlessEngineEnv set.
Data.Load ??= new Load { data = new LoadDetail() };
// CardParameter(CardCSVData) reads Data.Crossover.RestrictedCard for the deck-limit calc;
// an empty Crossover returns the default count (no restriction). Private setter -> reflect.
// Only set when null so we coexist with HeadlessEngineEnv.
if (Data.Crossover == null)
{
typeof(Data).GetProperty("Crossover",
BindingFlags.Static | BindingFlags.Public)!
.SetValue(null, new Crossover());
}
// Suppress VFX / take the virtual-battle resolution path (no live view layer).
BattleManagerBase.IsForecast = true;
// 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).
// --- static CardMaster (full cards.json) ----------------------------------------------
// 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) ---------------------------------
// 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();
// --- 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 (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, "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.
_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()
{
// --- 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();
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)
{
var netUser = new NetworkUserInfoData();
netUser.SetSelfInfo(
new Dictionary { ["fieldId"] = 1 },
isWatchReplayRecovery: false);
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
}
}
// --- CardMaster (full load) ----------------------------------------------------------------------
// Production difference (1): enumerate EVERY card row — no want.Contains(id) filter.
private static void LoadFullCardMaster()
{
var rows = new List();
using (var doc = JsonDocument.Parse(File.ReadAllText(CardsJsonPath)))
{
int sort = 0;
foreach (var el in doc.RootElement.EnumerateArray())
{
if (!el.TryGetProperty("card_id", out var idEl)) continue;
if (!int.TryParse(idEl.GetString(), out _)) continue; // skip malformed ids
rows.Add(BuildCardCsvData(el, sort++));
}
}
var cm = NewCardMaster(rows);
InjectAsDefault(cm);
}
// Transcribed from HeadlessCardMaster.BuildCardCsvData.
private static CardCSVData BuildCardCsvData(JsonElement el, int sortIndex)
{
var c = (CardCSVData)FormatterServices.GetUninitializedObject(typeof(CardCSVData));
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
foreach (var prop in el.EnumerateObject())
{
string val = prop.Value.ValueKind == JsonValueKind.Null ? null : prop.Value.ToString();
var f = typeof(CardCSVData).GetField(prop.Name, bf);
if (f != null) { SetMember(f.FieldType, val, v => f.SetValue(c, v)); continue; }
var p = typeof(CardCSVData).GetProperty(prop.Name, bf);
if (p != null && p.CanWrite) SetMember(p.PropertyType, val, v => p.SetValue(c, v));
}
var si = typeof(CardCSVData).GetProperty("SortIndex", bf);
if (si != null && si.CanWrite) si.SetValue(c, sortIndex);
return c;
}
private static void SetMember(Type t, string val, Action