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

@@ -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;
}
}