feat(engine-ambient): delete static fallbacks; add MultiInstanceEngineTests

Step 8 (final) of multi-instancing migration. All per-battle statics now
require a BattleAmbient scope — unwrapped writes throw InvalidOperationException
(fail-fast forcing function). MultiInstanceEngineTests proves correctness:
two parallel battles resolve independently, N=4/8/16 stress matches sequential
baseline, GameMgr.GetIns throws without scope.

Migration complete. EngineSessionGate gone. Suite fully green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 23:19:37 -04:00
parent 9e93a7b198
commit c789d836f1
15 changed files with 449 additions and 131 deletions

View File

@@ -62,7 +62,7 @@ BattleFinishToOpponentDisConnectChecker.cs BattleFinishToOpponentDisConnectCheck
BattleKeywordInfoListMgr.cs BattleKeywordInfoListMgr.cs a014170d0b3f5499635bcc2e29755dc2f3125d5a5a28b1741a4abc74b4abcf86 0
BattleLifeTimeSharedObject.cs BattleLifeTimeSharedObject.cs ab8bc3703d268752a1de56ab5d3e9ebd276980c20076eb0ca300838b3db13d5f 0
BattleLogTextBuilderAttachSkill.cs BattleLogTextBuilderAttachSkill.cs 11c585ae931fa3dc734bb231d6da61df3b51b803516ca2c5d88a0c78bc7c0104 0
BattleManagerBase.cs BattleManagerBase.cs d12aed345f596e068bfb130a247e280457cf5160a76f8bfef4a1dda1c561d196 1
BattleManagerBase.cs BattleManagerBase.cs c40684d0f3e3c11e603b374ee9ba64f1830052723ea183d0b90d76d4a646ad5c 1
BattleMenuMgr.cs BattleMenuMgr.cs 7418699063e01641d0df1ed16773a9ac9418f418cc047fc18c5892eb7971d361 0
BattlePlayer.cs BattlePlayer.cs 001409844b46ddaf0a5edbce4e015749ece61053adf725a978987d7063a02632 0
BattlePlayerBase.cs BattlePlayerBase.cs 9d3a665158706460a52900008dcfcdf575dbe08cb6d3cc05e63e718b2885b51b 0
@@ -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 ea018062d8823eb3527e7fd5e07d094a6fd444c1ad5a368bcf9132315cac99ac 1
Cute\Certification.cs Cute\Certification.cs 0491630cc02f155829e96d2a3624f13a81200b5a00fd253f54cefbe576371d70 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 88529d834ee64a12288ae2a40062824ea594f6d1b6c95d3cab2776f499369e77 1
Wizard\Data.cs Wizard\Data.cs 46db6a70b06a6ebf89f7210e32385a63ab82a8ae196518a684a3a632ff5dea69 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 ac0bde1ef076672c6e19969c5f9ec172aee79e7d5ee5bdf660e89166e8ff07ce 1
Wizard\ToolboxGame.cs Wizard\ToolboxGame.cs 968903c18a720c29cb410690d8912ff42d11dc7c41e29a61f77f8de112f18414 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
1 # engine-relpath source-relpath sha256 patched(0|1)
62 BattleKeywordInfoListMgr.cs BattleKeywordInfoListMgr.cs a014170d0b3f5499635bcc2e29755dc2f3125d5a5a28b1741a4abc74b4abcf86 0
63 BattleLifeTimeSharedObject.cs BattleLifeTimeSharedObject.cs ab8bc3703d268752a1de56ab5d3e9ebd276980c20076eb0ca300838b3db13d5f 0
64 BattleLogTextBuilderAttachSkill.cs BattleLogTextBuilderAttachSkill.cs 11c585ae931fa3dc734bb231d6da61df3b51b803516ca2c5d88a0c78bc7c0104 0
65 BattleManagerBase.cs BattleManagerBase.cs d12aed345f596e068bfb130a247e280457cf5160a76f8bfef4a1dda1c561d196 c40684d0f3e3c11e603b374ee9ba64f1830052723ea183d0b90d76d4a646ad5c 1
66 BattleMenuMgr.cs BattleMenuMgr.cs 7418699063e01641d0df1ed16773a9ac9418f418cc047fc18c5892eb7971d361 0
67 BattlePlayer.cs BattlePlayer.cs 001409844b46ddaf0a5edbce4e015749ece61053adf725a978987d7063a02632 0
68 BattlePlayerBase.cs BattlePlayerBase.cs 9d3a665158706460a52900008dcfcdf575dbe08cb6d3cc05e63e718b2885b51b 0
178 Cute\BootApp.cs Cute\BootApp.cs 1a6b3066b6155aba225b9ca4e79655e428fc7b24cb16569717b53600b1f23bb5 0
179 Cute\BootNetwork.cs Cute\BootNetwork.cs 99769dd6c38b2ee2ef7ad02e14530c658576e5c4a2188ed1cbcdd563f68443f6 0
180 Cute\BootSystem.cs Cute\BootSystem.cs 61afa7a7c8df705504031629965440d491603dec531b047a36b9294f255ee04e 0
181 Cute\Certification.cs Cute\Certification.cs ea018062d8823eb3527e7fd5e07d094a6fd444c1ad5a368bcf9132315cac99ac 0491630cc02f155829e96d2a3624f13a81200b5a00fd253f54cefbe576371d70 1
182 Cute\CryptAES.cs Cute\CryptAES.cs d3022b9e1be75bc07e57aef61093717a84b60383608eba5daa9b7dc6605b1e75 0
183 Cute\Cryptographer.cs Cute\Cryptographer.cs f61bc1f4727d323004c443c9db30a4f221db3309be2cb14d6f51fc6a39590908 0
184 Cute\CustomPreference.cs Cute\CustomPreference.cs ddc46cc13bf2728e4fcee7db550ec093ec3acf651ea48d3dbb5f5099d5a20f89 0
2624 Wizard\CsvColumns.cs Wizard\CsvColumns.cs d113f92e1f0145adb323c093deca81aab1889e8de34ed78c852b8e5a764c1c4c 0
2625 Wizard\CustomEasing.cs Wizard\CustomEasing.cs c7ac36e40e66f046d42e1d688db22f2acc2567399ce23c0512e3e2c8beefa598 0
2626 Wizard\DamagedTagCollection.cs Wizard\DamagedTagCollection.cs 8e6ecf677b4da8e16a68d3336959cd6d08af4830a0214297d65e96243f777c3f 0
2627 Wizard\Data.cs Wizard\Data.cs 88529d834ee64a12288ae2a40062824ea594f6d1b6c95d3cab2776f499369e77 46db6a70b06a6ebf89f7210e32385a63ab82a8ae196518a684a3a632ff5dea69 1
2628 Wizard\DeckAttributeType.cs Wizard\DeckAttributeType.cs 006bb4c04d8a60c9caf04873dde6c962366348db03ec40a8bbc0071392f656dd 0
2629 Wizard\DeckAutoCreateTask.cs Wizard\DeckAutoCreateTask.cs e8c21d513114d2c42ad85a28da4adb48642bd36dc012f6029fbc8a8d72b78d6c 0
2630 Wizard\DeckBuildShortageCardView.cs Wizard\DeckBuildShortageCardView.cs 34428e4efb614c4fd59136d1bb87485ce117a97b2c6668f0481fff4b510a31d3 0
3237 Wizard\TargetTagCollection.cs Wizard\TargetTagCollection.cs 1bd2fb66e58c9fae3d23fbce351972577da664c2b959ee92e0547e2396c82eb9 0
3238 Wizard\TextLineCreater.cs Wizard\TextLineCreater.cs fdb7f0a918c2f5b92268954b3980724f29cb16a8098ea67c2d99633ae5bd1e92 0
3239 Wizard\TokenPlayPattern.cs Wizard\TokenPlayPattern.cs c14d846afb81b876291013c077cbd503bebd27b651b5b78708e93d68758e2e7b 0
3240 Wizard\ToolboxGame.cs Wizard\ToolboxGame.cs ac0bde1ef076672c6e19969c5f9ec172aee79e7d5ee5bdf660e89166e8ff07ce 968903c18a720c29cb410690d8912ff42d11dc7c41e29a61f77f8de112f18414 1
3241 Wizard\TournamentCell.cs Wizard\TournamentCell.cs b07b968c6768b74b9345ab3aa1ae138950de022b08f92a16966b3daef81d379c 0
3242 Wizard\TournamentCellData.cs Wizard\TournamentCellData.cs ecc457bdf2dbaa7db9de893f69a07ac43e589ca140dad29866d658af386e605e 0
3243 Wizard\TournamentController.cs Wizard\TournamentController.cs 236122aa73487c600114c9b0010ab9e9c85a5745a267550088c8f790532647c9 0

View File

@@ -410,26 +410,14 @@ public class BattleManagerBase
private NetworkTouchControl _networkTouchControl;
private static BattleManagerBase _mainFallback;
private static bool _isRandomDrawFallback = false;
public static bool IsRandomDraw {
get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.IsRandomDraw ?? _isRandomDrawFallback;
set {
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
if (c != null) c.IsRandomDraw = value;
else _isRandomDrawFallback = value;
}
get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsRandomDraw;
set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsRandomDraw = value;
}
private static bool _isForecastFallback = false;
public static bool IsForecast {
get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.IsForecast ?? _isForecastFallback;
set {
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
if (c != null) c.IsForecast = value;
else _isForecastFallback = value;
}
get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsForecast;
set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsForecast = value;
}
public BattleLifeTimeSharedObject BattleLifeTimeSharedObject;
@@ -724,12 +712,15 @@ public class BattleManagerBase
public static BattleManagerBase GetIns()
{
return SVSim.BattleEngine.Ambient.BattleAmbient.Current?.Mgr ?? _mainFallback;
// Soft read: returns null when no scope is active, preserving engine call sites that pattern
// `GetIns()?.Foo ?? default` (or null-tolerant successors). Inside a scope, returns the
// scope's Mgr (which may itself be null mid-Setup — see SessionBattleEngine.SetupInternal,
// which publishes Mgr before WireMulliganPhase).
return SVSim.BattleEngine.Ambient.BattleAmbient.Current?.Mgr;
}
protected BattleManagerBase(IBattleMgrContentsCreator contentsCreator)
{
_mainFallback = this;
BattleLifeTimeSharedObject = new BattleLifeTimeSharedObject();
PublishedSkillList = new List<SkillBase>();
_contentsCreator = contentsCreator;
@@ -1488,7 +1479,6 @@ public class BattleManagerBase
GameMgr.GetIns().GetPrefabMgr().DisposeAllClonedObject();
GameMgr.GetIns().GetGameObjMgr().DisposeUIGameObj();
GameMgr.GetIns().GetPrefabMgr().AllUnLoad();
_mainFallback = null;
}
private void DisposeBattleGameObj_DestroyImmediate(GameObject obj)

View File

@@ -16,8 +16,6 @@ public class Certification : MonoBehaviour
private static string udid;
private static int _viewerIdFallback;
private static int short_udid;
private static string sessionId;
@@ -40,28 +38,13 @@ public class Certification : MonoBehaviour
public static int ViewerId
{
get
{
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
if (c != null) return c.ViewerId;
if (_viewerIdFallback == 0)
{
_viewerIdFallback = Toolbox.SavedataManager.GetInt("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();
_viewerIdFallback = value;
}
// Post-Task-8: strictly ambient. The historical SavedataManager-backed lazy decode was the
// client process's single-viewer-id source; in the headless multi-instance world the viewer
// id MUST come from the per-session ambient context. Setter is a no-op (BattleAmbientContext
// .ViewerId is `init`-only — fixed at scope entry per design — and the historical caller
// (SavedataManager.SetInt + Save) is dead in the server world).
get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().ViewerId;
set { /* ambient ViewerId is init-only; SavedataManager path is dead headless */ }
}
public static int ShortUdid
@@ -152,7 +135,6 @@ public class Certification : MonoBehaviour
{
sessionId = null;
udid = null;
_viewerIdFallback = 0;
short_udid = 0;
Toolbox.SavedataManager.SetInt("VIEWER_ID", 0);
Toolbox.SavedataManager.SetInt("SHORT_UDID", 0);

View File

@@ -176,16 +176,16 @@ public static class Data
public static ReplayDetailInfo ReplayBattleInfo { 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;
}
// Soft read: returns null when no scope is active. The MulliganMgrBase.StartDeal call site
// reads this with a null-tolerant ??=-style pattern, so a null degrade is the historical
// fallback. Inside a scope, returns the per-session RecoveryInfo (SessionBattleEngine
// pre-seeds an uninitialized BattleRecoveryInfo on its ctx field initializer).
get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.RecoveryInfo;
// Strict setter: writes must land on the per-session ctx. No historical production caller
// writes this outside a scope; an unwrapped write now fails fast (forcing function).
set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().RecoveryInfo = value;
}
public static VoteData VoteInfo { get; set; }

View File

@@ -25,10 +25,12 @@ public static class ToolboxGame
private static Transform _gameTransform = null;
private static RealTimeNetworkAgent _realTimeNetworkAgentFallback;
public static RealTimeNetworkAgent RealTimeNetworkAgent
{
get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent ?? _realTimeNetworkAgentFallback;
// Soft read: returns null when no scope is active, mirroring GetIns. Engine code reads this
// via `ToolboxGame.RealTimeNetworkAgent?.Foo`-style patterns (network-send paths gated on a
// non-null agent), so a null on the unwrapped path is the correct degrade — not a throw.
get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent;
}
public static Transform GameTransform
@@ -60,20 +62,19 @@ public static class ToolboxGame
public static void SetRealTimeNetworkBattle(RealTimeNetworkAgent agent)
{
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
if (c != null) c.NetworkAgent = agent;
else _realTimeNetworkAgentFallback = agent;
// Strict: must be inside a scope to set the per-session agent. Forcing-function — any
// historical unwrapped caller (no production callsite remains) now fails fast.
SVSim.BattleEngine.Ambient.BattleAmbient.Require().NetworkAgent = agent;
}
public static void DestroyNetworkAgent()
{
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
var current = c?.NetworkAgent ?? _realTimeNetworkAgentFallback;
var current = c?.NetworkAgent;
if (current != null)
{
Object.DestroyImmediate(current.gameObject);
if (c != null) c.NetworkAgent = null;
else _realTimeNetworkAgentFallback = null;
c.NetworkAgent = null;
}
}

View File

@@ -0,0 +1,77 @@
Multi-instancing migration (Step 8 — forcing function): delete the static per-battle fallbacks
the earlier ambient-* patches added as a coexistence shim. With the fallbacks gone, any unwrapped
engine call into IsForecast/IsRandomDraw fails fast (BattleAmbient.Require() throws
InvalidOperationException); GetIns soft-returns null when no scope is active so engine code
that patterns `GetIns()?.Foo ?? default` keeps composing (per design — strict for setters /
load-bearing reads, soft for the GetIns null-tolerant chain). Supersedes the intermediate
.ambient-isforecast-israndomdraw + .ambient-main-getins patches (kept for the audit trail).
Semantics chosen per accessor:
- IsForecast : get/set both BattleAmbient.Require() — strict (must be in a scope)
- IsRandomDraw : get/set both BattleAmbient.Require() — strict
- GetIns() : Current?.Mgr — soft (null when no scope; preserves engine `?.` patterns)
- ctor : `_mainFallback = this` line deleted entirely
- DisposeManager: `_mainFallback = null` line deleted entirely
In-file edits:
- line ~413 declaration `private static BattleManagerBase _mainFallback;` DELETED
- lines ~415-432 the four fallback statics + their if-else fallback branches DELETED,
replaced with strict get/set via BattleAmbient.Require()
- line ~727 GetIns() body -> Current?.Mgr (no fallback)
- line ~732 ctor `_mainFallback = this` DELETED (no longer needed; ambient.Mgr is published
explicitly from SessionBattleEngine.SetupInternal after construction)
- line ~1491 dispose `_mainFallback = null` DELETED (the matching scope drop replaces it)
--- Engine/BattleManagerBase.cs (~lines 413-433)
- private static BattleManagerBase _mainFallback;
-
- private static bool _isRandomDrawFallback = false;
- public static bool IsRandomDraw {
- get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.IsRandomDraw ?? _isRandomDrawFallback;
- set {
- var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
- if (c != null) c.IsRandomDraw = value;
- else _isRandomDrawFallback = value;
- }
- }
-
- private static bool _isForecastFallback = false;
- public static bool IsForecast {
- get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.IsForecast ?? _isForecastFallback;
- set {
- var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
- if (c != null) c.IsForecast = value;
- else _isForecastFallback = value;
- }
- }
+ public static bool IsRandomDraw {
+ get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsRandomDraw;
+ set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsRandomDraw = value;
+ }
+
+ public static bool IsForecast {
+ get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsForecast;
+ set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().IsForecast = value;
+ }
--- Engine/BattleManagerBase.cs (~line 727)
public static BattleManagerBase GetIns()
{
- return SVSim.BattleEngine.Ambient.BattleAmbient.Current?.Mgr ?? _mainFallback;
+ // Soft read: returns null when no scope is active, preserving engine call sites that
+ // pattern `GetIns()?.Foo ?? default`. Inside a scope, returns the scope's Mgr (which may
+ // itself be null mid-Setup — see SessionBattleEngine.SetupInternal, which publishes Mgr
+ // before WireMulliganPhase).
+ return SVSim.BattleEngine.Ambient.BattleAmbient.Current?.Mgr;
}
--- Engine/BattleManagerBase.cs (~line 732)
protected BattleManagerBase(IBattleMgrContentsCreator contentsCreator)
{
- _mainFallback = this;
BattleLifeTimeSharedObject = new BattleLifeTimeSharedObject();
--- Engine/BattleManagerBase.cs (~line 1491)
GameMgr.GetIns().GetPrefabMgr().AllUnLoad();
- _mainFallback = null;
}

View File

@@ -0,0 +1,73 @@
Multi-instancing migration (Step 8 — forcing function): delete the static viewer-id fallback the
earlier .ambient-viewerid patch added as a coexistence shim. ViewerId is now strictly per-session
ambient (BattleAmbient.Require().ViewerId). Supersedes the intermediate .ambient-viewerid patch
(kept for the audit trail).
Semantics:
- get : BattleAmbient.Require().ViewerId — strict (throws when no scope is active)
- set : NO-OP. The historical caller (SavedataManager.SetInt + Save) is the client process's
persistent-storage path; in the headless multi-instance world the viewer id flows from the
session ctx (BattleAmbientContext.ViewerId, init-only at scope entry — see
SessionBattleEngine's field initializer, which seeds it from EngineGlobalInit.ThisViewerId).
Making the setter strict (Require()) would also work but degrades to a write that can't land
(init-only) — a comment-documented no-op is clearer about why the setter went dead.
Related (SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs): the reflection write that seeded
`Certification._viewerIdFallback` is now dead — the static is deleted. Replaced with a comment-
only marker so the design intent (ThisViewerId defines the engine's "player" perspective for the
IsRecovery target parse) stays discoverable from the global init path.
InitializeFileds(): the now-dead `_viewerIdFallback = 0;` line is also deleted (the line above and
below — SavedataManager.SetInt("VIEWER_ID", 0) — stay).
In-file edits:
- line ~19 declaration `private static int _viewerIdFallback;` DELETED
- lines ~41-65 the ambient+fallback get/set body REPLACED with strict get + no-op set
- line ~155 `_viewerIdFallback = 0;` inside InitializeFileds DELETED
--- Engine/Cute/Certification.cs (~line 19)
private static string udid;
-
- private static int _viewerIdFallback;
-
private static int short_udid;
--- Engine/Cute/Certification.cs (~lines 41-65)
public static int ViewerId
{
- get
- {
- var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
- if (c != null) return c.ViewerId;
- if (_viewerIdFallback == 0)
- {
- _viewerIdFallback = Toolbox.SavedataManager.GetInt("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();
- _viewerIdFallback = value;
- }
+ // Post-Task-8: strictly ambient. The historical SavedataManager-backed lazy decode was the
+ // client process's single-viewer-id source; in the headless multi-instance world the viewer
+ // id MUST come from the per-session ambient context. Setter is a no-op (BattleAmbientContext
+ // .ViewerId is `init`-only — fixed at scope entry per design — and the historical caller
+ // (SavedataManager.SetInt + Save) is dead in the server world).
+ get => SVSim.BattleEngine.Ambient.BattleAmbient.Require().ViewerId;
+ set { /* ambient ViewerId is init-only; SavedataManager path is dead headless */ }
}
--- Engine/Cute/Certification.cs (~line 155)
sessionId = null;
udid = null;
- _viewerIdFallback = 0;
short_udid = 0;

View File

@@ -0,0 +1,43 @@
Multi-instancing migration (Step 8 — forcing function): delete the static BattleRecoveryInfo
fallback the earlier .ambient-recoveryinfo patch added as a coexistence shim. BattleRecoveryInfo
is now strictly per-session ambient. Supersedes the intermediate .ambient-recoveryinfo patch
(kept for the audit trail).
Semantics:
- BattleRecoveryInfo (getter) : Current?.RecoveryInfo — soft (null when no scope). The
MulliganMgrBase.StartDeal call site reads this with a null-tolerant pattern, so a null
degrade is the historical fallback. Inside a scope, returns the per-session RecoveryInfo
(SessionBattleEngine pre-seeds an uninitialized BattleRecoveryInfo on its ctx field
initializer).
- BattleRecoveryInfo (setter) : BattleAmbient.Require().RecoveryInfo = value — strict
(writes must land on the per-session ctx; forcing function).
Related (SVSim.BattleNode/Sessions/Engine/EngineGlobalInit.cs): the dead `Data.BattleRecoveryInfo
= ...` write inside the `_done` lock block is also deleted (flagged by Task 7 reviewer). The
per-session ctx pre-seeds RecoveryInfo, so the process-global write was dead the moment the
ambient seam landed.
In-file edits:
- line ~179 declaration `private static BattleRecoveryInfo _battleRecoveryInfoFallback;` DELETED
- lines ~180-189 the ambient+fallback get/set body REPLACED with strict semantics
--- Engine/Wizard/Data.cs (~lines 179-189)
- 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;
- }
+ // Soft read: returns null when no scope is active. The MulliganMgrBase.StartDeal call site
+ // reads this with a null-tolerant ??=-style pattern, so a null degrade is the historical
+ // fallback. Inside a scope, returns the per-session RecoveryInfo (SessionBattleEngine
+ // pre-seeds an uninitialized BattleRecoveryInfo on its ctx field initializer).
+ get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.RecoveryInfo;
+ // Strict setter: writes must land on the per-session ctx. No historical production caller
+ // writes this outside a scope; an unwrapped write now fails fast (forcing function).
+ set => SVSim.BattleEngine.Ambient.BattleAmbient.Require().RecoveryInfo = value;
}

View File

@@ -0,0 +1,57 @@
Multi-instancing migration (Step 8 — forcing function): delete the static RealTimeNetworkAgent
fallback the earlier .ambient-rtna patch added as a coexistence shim. RealTimeNetworkAgent is now
strictly per-session ambient. Supersedes the intermediate .ambient-rtna patch (kept for the audit
trail).
Semantics:
- RealTimeNetworkAgent (getter) : Current?.NetworkAgent — soft (null when no scope).
Engine code reads this via `ToolboxGame.RealTimeNetworkAgent?.Foo`-style patterns (network
send paths gated on a non-null agent), so a null on the unwrapped path is the correct
degrade — not a throw.
- SetRealTimeNetworkBattle : BattleAmbient.Require().NetworkAgent = agent — strict (writes must
land on the per-session ctx; forcing function).
- DestroyNetworkAgent : reads Current, destroys, nulls the ambient NetworkAgent in-place.
Outside a scope is a no-op (nothing to destroy in the ambient world).
In-file edits:
- line ~28 declaration `private static RealTimeNetworkAgent _realTimeNetworkAgentFallback;` DELETED
- lines ~29-32 getter body no fallback chain
- lines ~61-66 Set body Require() (strict)
- lines ~68-78 Destroy body Current-based; no fallback branch
--- Engine/Wizard/ToolboxGame.cs (~lines 28-32)
- private static RealTimeNetworkAgent _realTimeNetworkAgentFallback;
public static RealTimeNetworkAgent RealTimeNetworkAgent
{
- get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent ?? _realTimeNetworkAgentFallback;
+ // Soft read: returns null when no scope is active, mirroring GetIns. Engine code reads
+ // this via `ToolboxGame.RealTimeNetworkAgent?.Foo`-style patterns (network-send paths
+ // gated on a non-null agent), so a null on the unwrapped path is the correct degrade —
+ // not a throw.
+ get => SVSim.BattleEngine.Ambient.BattleAmbient.Current?.NetworkAgent;
}
--- Engine/Wizard/ToolboxGame.cs (~lines 61-78)
public static void SetRealTimeNetworkBattle(RealTimeNetworkAgent agent)
{
- var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
- if (c != null) c.NetworkAgent = agent;
- else _realTimeNetworkAgentFallback = agent;
+ // Strict: must be inside a scope to set the per-session agent. Forcing-function — any
+ // historical unwrapped caller (no production callsite remains) now fails fast.
+ SVSim.BattleEngine.Ambient.BattleAmbient.Require().NetworkAgent = agent;
}
public static void DestroyNetworkAgent()
{
var c = SVSim.BattleEngine.Ambient.BattleAmbient.Current;
- var current = c?.NetworkAgent ?? _realTimeNetworkAgentFallback;
+ var current = c?.NetworkAgent;
if (current != null)
{
Object.DestroyImmediate(current.gameObject);
- if (c != null) c.NetworkAgent = null;
- else _realTimeNetworkAgentFallback = null;
+ c.NetworkAgent = null;
}
}