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 set) { if (t == typeof(string)) set(val); else if (t == typeof(int)) set(int.TryParse(val, out var i) ? i : 0); else if (t == typeof(bool)) set(val == "1" || string.Equals(val, "true", StringComparison.OrdinalIgnoreCase)); // other types left at default } private static CardMaster NewCardMaster(List rows) { var ctor = typeof(CardMaster).GetConstructor( BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(List) }, null); if (ctor == null) throw new InvalidOperationException("CardMaster(List) ctor not found"); return (CardMaster)ctor.Invoke(new object[] { rows }); } private static void InjectAsDefault(CardMaster cm) { var idType = typeof(CardMaster).GetNestedType("CardMasterId"); var defaultId = Enum.Parse(idType, "Default"); var dictType = typeof(Dictionary<,>).MakeGenericType(idType, typeof(CardMaster)); var dict = (IDictionary)Activator.CreateInstance(dictType); dict[defaultId] = cm; var fld = typeof(CardMaster).GetField("_dictCardMaster", BindingFlags.Static | BindingFlags.NonPublic); fld.SetValue(null, dict); } // --- Master reference data (all 8 classes) ------------------------------------------------------- // Transcribed from HeadlessMasterData.Install. Production difference (2): install ALL 8 classes. private static void InstallMaster() { var master = (Master)FormatterServices.GetUninitializedObject(typeof(Master)); EnsureEmptyCollections(master); var list = new List(); for (int c = 1; c <= 8; c++) list.Add(NewChara(c, c)); // charaId == classId for class c SetMember(master, "ClassCharacterList", list); Data.Master = master; } private static void EnsureEmptyCollections(object obj) { const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; foreach (var f in obj.GetType().GetFields(bf)) { if (f.GetValue(obj) != null) continue; var empty = EmptyOf(f.FieldType); if (empty != null) f.SetValue(obj, empty); } } private static object EmptyOf(Type t) { if (t.IsArray) return Array.CreateInstance(t.GetElementType(), 0); if (t.IsGenericType) { var def = t.GetGenericTypeDefinition(); if (def == typeof(List<>) || def == typeof(Dictionary<,>) || def == typeof(HashSet<>) || def == typeof(IList<>) || def == typeof(IDictionary<,>) || def == typeof(ICollection<>) || def == typeof(IEnumerable<>)) { var concrete = def == typeof(List<>) || def == typeof(IList<>) || def == typeof(ICollection<>) || def == typeof(IEnumerable<>) ? typeof(List<>).MakeGenericType(t.GetGenericArguments()) : def == typeof(HashSet<>) ? typeof(HashSet<>).MakeGenericType(t.GetGenericArguments()) : typeof(Dictionary<,>).MakeGenericType(t.GetGenericArguments()); return Activator.CreateInstance(concrete); } } return null; } private static ClassCharacterMasterData NewChara(int charaId, int classId) { var c = (ClassCharacterMasterData)FormatterServices.GetUninitializedObject(typeof(ClassCharacterMasterData)); SetMember(c, "chara_id", charaId); SetMember(c, "class_id", classId); SetMember(c, "skin_id", charaId); SetMember(c, "is_usable", true); return c; } // --- reflection helpers (transcribed from the test fixtures) -------------------------------------- // Set a member (auto-property backing field or field) by name, tolerating private setters. private static void SetMember(object obj, string name, object value) { var t = obj.GetType(); const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var p = t.GetProperty(name, bf); if (p != null && p.SetMethod != null) { p.SetValue(obj, value); return; } var f = t.GetField(name, bf) ?? t.GetField($"<{name}>k__BackingField", bf); if (f != null) { f.SetValue(obj, value); return; } throw new InvalidOperationException($"{t.Name} has no settable member '{name}'"); } // Idempotent backing-field set: only writes when the field is currently 0 (int) or null. private static void SetFieldIfZeroOrNull(object obj, string name, object value) { var f = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new InvalidOperationException($"{obj.GetType().Name} has no field '{name}'"); var cur = f.GetValue(obj); if (cur is null || (cur is int i && i == 0)) f.SetValue(obj, value); } }