From fe146fde50f00ff2dbc915bac8a6599d1e4a3e09 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 7 Jun 2026 21:37:58 -0400 Subject: [PATCH] refactor(engine-ambient): ViewerId/RealTimeNetworkAgent/BattleRecoveryInfo read ambient first Step 4 of multi-instancing migration. Three additional per-battle statics front-fronted by BattleAmbient.Current, each with a static fallback for unwrapped callers. ViewerId's SavedataManager-persisting setter is preserved on the fallback path; inside a scope, the setter is a no-op (the per-battle perspective is fixed at scope entry). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../BattleAmbientTests.cs | 41 +++++++++++++ SVSim.BattleEngine/COPIED.manifest.tsv | 6 +- .../Engine/Cute/Certification.cs | 20 ++++-- SVSim.BattleEngine/Engine/Wizard/Data.cs | 12 +++- .../Engine/Wizard/ToolboxGame.cs | 19 ++++-- .../Certification.ambient-viewerid.patch | 61 +++++++++++++++++++ .../Patches/Data.ambient-recoveryinfo.patch | 34 +++++++++++ .../Patches/ToolboxGame.ambient-rtna.patch | 51 ++++++++++++++++ .../Sessions/Engine/EngineGlobalInit.cs | 2 +- 9 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 SVSim.BattleEngine/Patches/Certification.ambient-viewerid.patch create mode 100644 SVSim.BattleEngine/Patches/Data.ambient-recoveryinfo.patch create mode 100644 SVSim.BattleEngine/Patches/ToolboxGame.ambient-rtna.patch diff --git a/SVSim.BattleEngine.Tests/BattleAmbientTests.cs b/SVSim.BattleEngine.Tests/BattleAmbientTests.cs index 77cc0ec..e4a6ca2 100644 --- a/SVSim.BattleEngine.Tests/BattleAmbientTests.cs +++ b/SVSim.BattleEngine.Tests/BattleAmbientTests.cs @@ -148,4 +148,45 @@ public class BattleAmbientTests var v = BattleManagerBase.GetIns(); Assert.Pass($"GetIns()={(v is null ? "null" : v.GetType().Name)}"); } + + [Test] + public void ViewerId_ReadsAmbient_WhenScopeActive() + { + var ctx = new BattleAmbientContext { ViewerId = 12345 }; + using var _ = BattleAmbient.Enter(ctx); + Assert.That(Cute.Certification.ViewerId, Is.EqualTo(12345)); + } + + [Test] + public void RealTimeNetworkAgent_ReadsAmbient_WhenScopeActive() + { + var ctx = new BattleAmbientContext(); + using var _ = BattleAmbient.Enter(ctx); + Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Null); + var agent = (RealTimeNetworkAgent)System.Runtime.Serialization + .FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent)); + ctx.NetworkAgent = agent; + Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.SameAs(agent)); + } + + [Test] + public void SetRealTimeNetworkBattle_InsideScope_WritesAmbient() + { + var ctx = new BattleAmbientContext(); + using var _ = BattleAmbient.Enter(ctx); + var agent = (RealTimeNetworkAgent)System.Runtime.Serialization + .FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent)); + Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent); + Assert.That(ctx.NetworkAgent, Is.SameAs(agent)); + } + + [Test] + public void BattleRecoveryInfo_ReadsAmbient_WhenScopeActive() + { + var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization + .FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo)); + var ctx = new BattleAmbientContext { RecoveryInfo = info }; + using var _ = BattleAmbient.Enter(ctx); + Assert.That(Wizard.Data.BattleRecoveryInfo, Is.SameAs(info)); + } } diff --git a/SVSim.BattleEngine/COPIED.manifest.tsv b/SVSim.BattleEngine/COPIED.manifest.tsv index 9040327..798bd03 100644 --- a/SVSim.BattleEngine/COPIED.manifest.tsv +++ b/SVSim.BattleEngine/COPIED.manifest.tsv @@ -178,7 +178,7 @@ Cute\AudioManager.cs Cute\AudioManager.cs 5e0cce94bcfad63266cd298aef89bb383e541f Cute\BootApp.cs Cute\BootApp.cs 1a6b3066b6155aba225b9ca4e79655e428fc7b24cb16569717b53600b1f23bb5 0 Cute\BootNetwork.cs Cute\BootNetwork.cs 99769dd6c38b2ee2ef7ad02e14530c658576e5c4a2188ed1cbcdd563f68443f6 0 Cute\BootSystem.cs Cute\BootSystem.cs 61afa7a7c8df705504031629965440d491603dec531b047a36b9294f255ee04e 0 -Cute\Certification.cs Cute\Certification.cs 8c143ee5f98e99332bbd1d6ec091d7590004b3b7215f1dc234d39bb5402f8531 0 +Cute\Certification.cs Cute\Certification.cs ea018062d8823eb3527e7fd5e07d094a6fd444c1ad5a368bcf9132315cac99ac 1 Cute\CryptAES.cs Cute\CryptAES.cs d3022b9e1be75bc07e57aef61093717a84b60383608eba5daa9b7dc6605b1e75 0 Cute\Cryptographer.cs Cute\Cryptographer.cs f61bc1f4727d323004c443c9db30a4f221db3309be2cb14d6f51fc6a39590908 0 Cute\CustomPreference.cs Cute\CustomPreference.cs ddc46cc13bf2728e4fcee7db550ec093ec3acf651ea48d3dbb5f5099d5a20f89 0 @@ -2624,7 +2624,7 @@ Wizard\CrossoverRewardInfo.cs Wizard\CrossoverRewardInfo.cs bb3763306d0e7d3feefb Wizard\CsvColumns.cs Wizard\CsvColumns.cs d113f92e1f0145adb323c093deca81aab1889e8de34ed78c852b8e5a764c1c4c 0 Wizard\CustomEasing.cs Wizard\CustomEasing.cs c7ac36e40e66f046d42e1d688db22f2acc2567399ce23c0512e3e2c8beefa598 0 Wizard\DamagedTagCollection.cs Wizard\DamagedTagCollection.cs 8e6ecf677b4da8e16a68d3336959cd6d08af4830a0214297d65e96243f777c3f 0 -Wizard\Data.cs Wizard\Data.cs 26bd39c591da328c1a30eccf773064be022145dad1f11a29584832b8a81d36e1 0 +Wizard\Data.cs Wizard\Data.cs 88529d834ee64a12288ae2a40062824ea594f6d1b6c95d3cab2776f499369e77 1 Wizard\DeckAttributeType.cs Wizard\DeckAttributeType.cs 006bb4c04d8a60c9caf04873dde6c962366348db03ec40a8bbc0071392f656dd 0 Wizard\DeckAutoCreateTask.cs Wizard\DeckAutoCreateTask.cs e8c21d513114d2c42ad85a28da4adb48642bd36dc012f6029fbc8a8d72b78d6c 0 Wizard\DeckBuildShortageCardView.cs Wizard\DeckBuildShortageCardView.cs 34428e4efb614c4fd59136d1bb87485ce117a97b2c6668f0481fff4b510a31d3 0 @@ -3237,7 +3237,7 @@ Wizard\TargetSelectType.cs Wizard\TargetSelectType.cs 91ab18f9c069784e1140a187ea Wizard\TargetTagCollection.cs Wizard\TargetTagCollection.cs 1bd2fb66e58c9fae3d23fbce351972577da664c2b959ee92e0547e2396c82eb9 0 Wizard\TextLineCreater.cs Wizard\TextLineCreater.cs fdb7f0a918c2f5b92268954b3980724f29cb16a8098ea67c2d99633ae5bd1e92 0 Wizard\TokenPlayPattern.cs Wizard\TokenPlayPattern.cs c14d846afb81b876291013c077cbd503bebd27b651b5b78708e93d68758e2e7b 0 -Wizard\ToolboxGame.cs Wizard\ToolboxGame.cs f0a39a9ec7a06d08cc2594cc93f010c602946492ed93c002775c41000f30ef75 0 +Wizard\ToolboxGame.cs Wizard\ToolboxGame.cs ac0bde1ef076672c6e19969c5f9ec172aee79e7d5ee5bdf660e89166e8ff07ce 1 Wizard\TournamentCell.cs Wizard\TournamentCell.cs b07b968c6768b74b9345ab3aa1ae138950de022b08f92a16966b3daef81d379c 0 Wizard\TournamentCellData.cs Wizard\TournamentCellData.cs ecc457bdf2dbaa7db9de893f69a07ac43e589ca140dad29866d658af386e605e 0 Wizard\TournamentController.cs Wizard\TournamentController.cs 236122aa73487c600114c9b0010ab9e9c85a5745a267550088c8f790532647c9 0 diff --git a/SVSim.BattleEngine/Engine/Cute/Certification.cs b/SVSim.BattleEngine/Engine/Cute/Certification.cs index 4620b63..a86bf22 100644 --- a/SVSim.BattleEngine/Engine/Cute/Certification.cs +++ b/SVSim.BattleEngine/Engine/Cute/Certification.cs @@ -16,7 +16,7 @@ public class Certification : MonoBehaviour private static string udid; - private static int viewer_id; + private static int _viewerIdFallback; private static int short_udid; @@ -42,17 +42,25 @@ public class Certification : MonoBehaviour { get { - if (viewer_id == 0) + var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; + if (c != null) return c.ViewerId; + if (_viewerIdFallback == 0) { - viewer_id = Toolbox.SavedataManager.GetInt("VIEWER_ID"); + _viewerIdFallback = Toolbox.SavedataManager.GetInt("VIEWER_ID"); } - return viewer_id; + return _viewerIdFallback; } set { + var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; + if (c != null) + { + // Inside a scope, ViewerId is fixed at scope entry — swallow the write. + return; + } Toolbox.SavedataManager.SetInt("VIEWER_ID", value); Toolbox.SavedataManager.Save(); - viewer_id = value; + _viewerIdFallback = value; } } @@ -144,7 +152,7 @@ public class Certification : MonoBehaviour { sessionId = null; udid = null; - viewer_id = 0; + _viewerIdFallback = 0; short_udid = 0; Toolbox.SavedataManager.SetInt("VIEWER_ID", 0); Toolbox.SavedataManager.SetInt("SHORT_UDID", 0); diff --git a/SVSim.BattleEngine/Engine/Wizard/Data.cs b/SVSim.BattleEngine/Engine/Wizard/Data.cs index 93fcfd0..6b43f6a 100644 --- a/SVSim.BattleEngine/Engine/Wizard/Data.cs +++ b/SVSim.BattleEngine/Engine/Wizard/Data.cs @@ -176,7 +176,17 @@ public static class Data public static ReplayDetailInfo ReplayBattleInfo { get; set; } - public static BattleRecoveryInfo BattleRecoveryInfo { get; set; } + private static BattleRecoveryInfo _battleRecoveryInfoFallback; + public static BattleRecoveryInfo BattleRecoveryInfo + { + get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.RecoveryInfo ?? _battleRecoveryInfoFallback; + set + { + var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; + if (c != null) c.RecoveryInfo = value; + else _battleRecoveryInfoFallback = value; + } + } public static VoteData VoteInfo { get; set; } diff --git a/SVSim.BattleEngine/Engine/Wizard/ToolboxGame.cs b/SVSim.BattleEngine/Engine/Wizard/ToolboxGame.cs index 6e0f497..bb2d91b 100644 --- a/SVSim.BattleEngine/Engine/Wizard/ToolboxGame.cs +++ b/SVSim.BattleEngine/Engine/Wizard/ToolboxGame.cs @@ -25,7 +25,11 @@ public static class ToolboxGame private static Transform _gameTransform = null; - public static RealTimeNetworkAgent RealTimeNetworkAgent { get; private set; } + private static RealTimeNetworkAgent _realTimeNetworkAgentFallback; + public static RealTimeNetworkAgent RealTimeNetworkAgent + { + get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent ?? _realTimeNetworkAgentFallback; + } public static Transform GameTransform { @@ -56,15 +60,20 @@ public static class ToolboxGame public static void SetRealTimeNetworkBattle(RealTimeNetworkAgent agent) { - RealTimeNetworkAgent = agent; + var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; + if (c != null) c.NetworkAgent = agent; + else _realTimeNetworkAgentFallback = agent; } public static void DestroyNetworkAgent() { - if (RealTimeNetworkAgent != null) + var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; + var current = c?.NetworkAgent ?? _realTimeNetworkAgentFallback; + if (current != null) { - Object.DestroyImmediate(RealTimeNetworkAgent.gameObject); - RealTimeNetworkAgent = null; + Object.DestroyImmediate(current.gameObject); + if (c != null) c.NetworkAgent = null; + else _realTimeNetworkAgentFallback = null; } } diff --git a/SVSim.BattleEngine/Patches/Certification.ambient-viewerid.patch b/SVSim.BattleEngine/Patches/Certification.ambient-viewerid.patch new file mode 100644 index 0000000..7d64911 --- /dev/null +++ b/SVSim.BattleEngine/Patches/Certification.ambient-viewerid.patch @@ -0,0 +1,61 @@ +Multi-instancing migration (Step 4): convert Cute.Certification.ViewerId — the per-battle +"who is the local seat" identity — to resolve through BattleAmbient.Current when a scope is +active, falling back to the legacy SavedataManager-backed static when not. The old `viewer_id` +backing field is renamed to `_viewerIdFallback` so unwrapped callers (real client, in-process +unit tests without a BattleAmbient scope) keep the original lazy-load-from-savedata + write- +through-savedata behavior bit-for-bit; scoped callers get a per-AsyncLocal id without writing +to savedata (design 2026-06-07-engine-multi-instancing, Task 4). + +Inside a scope the setter is a NO-OP: ViewerId is an init-time identity (it's set at scope +entry via `new BattleAmbientContext { ViewerId = ... }`'s init-only property), and swallowing +the write avoids any in-battle setter from mutating sibling-battle perspective. The fallback +setter preserves the original sequence exactly: SetInt + Save + assign backing field. + +In-file references (3 total) handled as follows: + - line 19 declaration → renamed to `_viewerIdFallback` + - line 41 ViewerId property → ambient-first getter; setter swallows when scoped + - line 147 InitializeFileds → resets the renamed `_viewerIdFallback` + +External reflection follow-up (NOT in this patch — separate edit, same commit): + SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs (~line 154) reads the private field + by name to seed the headless "who am I" perspective. Renamed there from "viewer_id" to + "_viewerIdFallback" to match. + +--- Engine/Cute/Certification.cs (~line 19) +- private static int viewer_id; ++ private static int _viewerIdFallback; + +--- Engine/Cute/Certification.cs (~lines 41-57) + public static int ViewerId + { + get + { +- if (viewer_id == 0) ++ var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; ++ if (c != null) return c.ViewerId; ++ if (_viewerIdFallback == 0) + { +- viewer_id = Toolbox.SavedataManager.GetInt("VIEWER_ID"); ++ _viewerIdFallback = Toolbox.SavedataManager.GetInt("VIEWER_ID"); + } +- return viewer_id; ++ return _viewerIdFallback; + } + set + { ++ var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; ++ if (c != null) ++ { ++ // Inside a scope, ViewerId is fixed at scope entry — swallow the write. ++ return; ++ } + Toolbox.SavedataManager.SetInt("VIEWER_ID", value); + Toolbox.SavedataManager.Save(); +- viewer_id = value; ++ _viewerIdFallback = value; + } + } + +--- Engine/Cute/Certification.cs (~line 147, InitializeFileds) +- viewer_id = 0; ++ _viewerIdFallback = 0; diff --git a/SVSim.BattleEngine/Patches/Data.ambient-recoveryinfo.patch b/SVSim.BattleEngine/Patches/Data.ambient-recoveryinfo.patch new file mode 100644 index 0000000..933589f --- /dev/null +++ b/SVSim.BattleEngine/Patches/Data.ambient-recoveryinfo.patch @@ -0,0 +1,34 @@ +Multi-instancing migration (Step 4): convert Wizard.Data.BattleRecoveryInfo — the per-battle +mid-game recovery snapshot (replay/reconnect/late-join state, owned by RecoveryDataHandler / +RecoveryNetworkManagerBase / RecoveryController) — to resolve through BattleAmbient.Current +when a scope is active, falling back to a `_battleRecoveryInfoFallback` static when not. +The original was a plain auto-property `{ get; set; }`; converting to a manual property + +ambient-aware getter/setter preserves the public read/write surface byte-identical +(design 2026-06-07-engine-multi-instancing, Task 4). + +This is the only of the three Step-4 conversions whose setter we DO route through ambient +(unlike ViewerId, where the in-scope setter is a no-op): recovery info is mutated during the +battle (new snapshots overwrite old ones as recovery checkpoints stream in), so scoped writes +must land on the AsyncLocal slot, not bleed into the process-wide fallback. + +The existing line-348 reset inside `Data.Clear()` (`BattleRecoveryInfo = null;`) keeps working +unchanged because it goes through the public setter, which now correctly routes to whichever +slot the caller's scope (or lack thereof) points at. + +In-file references (1 declaration; ~5 setter/getter call sites elsewhere in the file go through +the public property and need no edits) handled as follows: + - line 179 auto-property → manual ambient-first getter + ambient-routed setter + +--- Engine/Wizard/Data.cs (~line 179) +- public static BattleRecoveryInfo BattleRecoveryInfo { get; set; } ++ private static BattleRecoveryInfo _battleRecoveryInfoFallback; ++ public static BattleRecoveryInfo BattleRecoveryInfo ++ { ++ get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.RecoveryInfo ?? _battleRecoveryInfoFallback; ++ set ++ { ++ var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; ++ if (c != null) c.RecoveryInfo = value; ++ else _battleRecoveryInfoFallback = value; ++ } ++ } diff --git a/SVSim.BattleEngine/Patches/ToolboxGame.ambient-rtna.patch b/SVSim.BattleEngine/Patches/ToolboxGame.ambient-rtna.patch new file mode 100644 index 0000000..9724a75 --- /dev/null +++ b/SVSim.BattleEngine/Patches/ToolboxGame.ambient-rtna.patch @@ -0,0 +1,51 @@ +Multi-instancing migration (Step 4): convert Wizard.ToolboxGame.RealTimeNetworkAgent — the +per-battle handle to the live PvP/AI network agent (RealTimeNetworkAgent owns the Socket.IO +connection, OnEmit pipeline, and ack bookkeeping for one battle) — to resolve through +BattleAmbient.Current when a scope is active, falling back to a renamed +`_realTimeNetworkAgentFallback` static when not. The original was an auto-property with a +private setter, mutated only via SetRealTimeNetworkBattle / DestroyNetworkAgent; converting +to a manual property + ambient-aware mutators keeps the public surface byte-identical +(design 2026-06-07-engine-multi-instancing, Task 4). + +DestroyNetworkAgent preserves the original Unity DestroyImmediate(.gameObject) call exactly, +just reading the "current" agent through the same ambient-first resolution and clearing the +slot it came from. Object.DestroyImmediate resolves via the file's existing `using UnityEngine;` +(no qualifier change needed — matches the file's existing pattern at line 36/79 that calls +`GameObject.Find` unqualified). + +In-file references (3 sites) handled as follows: + - line 28 auto-property → renamed backing + manual ambient-first getter + - line 57 SetRealTimeNetworkBattle → writes through Current?.NetworkAgent or fallback + - line 62 DestroyNetworkAgent → reads current via ambient-first; clears the same slot + +--- Engine/Wizard/ToolboxGame.cs (~line 28) +- public static RealTimeNetworkAgent RealTimeNetworkAgent { get; private set; } ++ private static RealTimeNetworkAgent _realTimeNetworkAgentFallback; ++ public static RealTimeNetworkAgent RealTimeNetworkAgent ++ { ++ get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent ?? _realTimeNetworkAgentFallback; ++ } + +--- Engine/Wizard/ToolboxGame.cs (~lines 57-69) + public static void SetRealTimeNetworkBattle(RealTimeNetworkAgent agent) + { +- RealTimeNetworkAgent = agent; ++ var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; ++ if (c != null) c.NetworkAgent = agent; ++ else _realTimeNetworkAgentFallback = agent; + } + + public static void DestroyNetworkAgent() + { +- if (RealTimeNetworkAgent != null) ++ var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current; ++ var current = c?.NetworkAgent ?? _realTimeNetworkAgentFallback; ++ if (current != null) + { +- Object.DestroyImmediate(RealTimeNetworkAgent.gameObject); +- RealTimeNetworkAgent = null; ++ Object.DestroyImmediate(current.gameObject); ++ if (c != null) c.NetworkAgent = null; ++ else _realTimeNetworkAgentFallback = null; + } + } diff --git a/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs b/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs index 2fe438c..a157de6 100644 --- a/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs +++ b/SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs @@ -151,7 +151,7 @@ internal static class EngineGlobalInit // targetList[0]. Seed the backing field with a stable nonzero id so the getter short-circuits. // It defines the engine's "player" perspective: a target vid == ThisViewerId resolves on // BattlePlayer (engine seat A), vid != it on BattleEnemy (seat B). Only set when 0 (coexistence). - var viewerIdField = typeof(Certification).GetField("viewer_id", + var viewerIdField = typeof(Certification).GetField("_viewerIdFallback", BindingFlags.Static | BindingFlags.NonPublic)!; if ((int)(viewerIdField.GetValue(null) ?? 0) == 0) viewerIdField.SetValue(null, ThisViewerId);