feat(engine-ambient): add BattleAmbientContext + AsyncLocal scope

Step 1 of the engine multi-instancing migration. Standalone infrastructure;
no engine static reads/writes through it yet. Scope is reentrant (restores
prior on dispose), AsyncLocal flows across awaits, and isolated between
concurrent Task.Run flows.

See docs/superpowers/specs/2026-06-07-engine-multi-instancing-design.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 21:04:21 -04:00
parent addeb021d2
commit 4829e8c263
2 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
// AUTHORED SHIM (not copied). Per-battle ambient context that backs the
// AsyncLocal singleton seam for multi-instancing (see docs/superpowers/specs/
// 2026-06-07-engine-multi-instancing-design.md). The engine's per-battle
// statics (BattleManagerBase.main/IsForecast/IsRandomDraw, GameMgr.GetIns,
// Certification.viewer_id, ToolboxGame.RealTimeNetworkAgent,
// Data.BattleRecoveryInfo) resolve through Current when set; process-shared
// reference data (CardMaster.Default, Data.Master, etc.) stays static.
#nullable enable
using System;
using System.Threading;
namespace SVSim.BattleEngine.Ambient;
public sealed class BattleAmbientContext
{
public BattleManagerBase? Mgr { get; set; }
public GameMgr GameMgr { get; init; } = new();
public RealTimeNetworkAgent? NetworkAgent { get; set; }
public int ViewerId { get; init; } = 1001;
public bool IsForecast { get; set; } = true;
public bool IsRandomDraw { get; set; } = true;
public Wizard.BattleRecoveryInfo? RecoveryInfo { get; set; }
}
public static class BattleAmbient
{
internal static readonly AsyncLocal<BattleAmbientContext?> _current = new();
public static BattleAmbientContext? Current => _current.Value;
public static BattleAmbientContext Require() =>
_current.Value ?? throw new InvalidOperationException(
"No ambient battle context. Wrap engine entry points in BattleAmbient.Enter(ctx).");
public static Scope Enter(BattleAmbientContext ctx)
{
var prior = _current.Value;
_current.Value = ctx;
return new Scope(prior);
}
public readonly struct Scope : IDisposable
{
private readonly BattleAmbientContext? _prior;
internal Scope(BattleAmbientContext? prior) { _prior = prior; }
public void Dispose() => _current.Value = _prior;
}
}