refactor(engine-ambient): GameMgr.GetIns throws Require; wrap SessionBattleEngine entry points
Step 5 of multi-instancing migration. GameMgr.GetIns() now resolves through BattleAmbient.Require() (throws when no scope active — fail-fast since engine callers unconditionally dereference). SessionBattleEngine now owns a single BattleAmbientContext, pushed via BattleAmbient.Enter at Setup/Receive/all ~30 read accessors and Debug* seams. EngineGlobalInit.WirePerSessionGameMgr extracted out of the _done-gated block: GameMgr is now per-session (ctx.GameMgr is a fresh `new()` per SessionBattleEngine), so the DataMgr chara ids + NetworkUserInfoData seeding must run every Setup, not process-once. The wiring itself is already idempotent. Without this, second-or- later sessions in a process NRE in NetworkBattleManagerBase.CreateBackgroundId. Expected state: SVSim.BattleEngine.Tests have known-failing tests that don't go through SessionBattleEngine (Task 6 wraps HeadlessFixture). SVSim.UnitTests mostly recover; residual failures (deal-frame Accepted:false in conductor integration tests) are captured in data_dumps/task5-test-output/failing-tests-after-task5-node-postwrap.txt for Task 7. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,12 @@ internal static class EngineGlobalInit
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -107,29 +113,6 @@ internal static class EngineGlobalInit
|
||||
// the postcondition is all-8-class ClassCharacterList. Idempotent (replaces Data.Master).
|
||||
InstallMaster();
|
||||
|
||||
// --- 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<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
}
|
||||
|
||||
// --- 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
|
||||
@@ -160,6 +143,36 @@ internal static class EngineGlobalInit
|
||||
}
|
||||
}
|
||||
|
||||
// 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<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
}
|
||||
}
|
||||
|
||||
// --- CardMaster (full load) ----------------------------------------------------------------------
|
||||
|
||||
// Production difference (1): enumerate EVERY card row — no want.Contains(id) filter.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
extern alias engine;
|
||||
using BattleAmbient = engine::SVSim.BattleEngine.Ambient.BattleAmbient;
|
||||
using BattleAmbientContext = engine::SVSim.BattleEngine.Ambient.BattleAmbientContext;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using engine::SVSim.BattleEngine.Rng;
|
||||
@@ -47,6 +49,12 @@ internal sealed class SessionBattleEngine
|
||||
{
|
||||
private const int DefaultLeaderLife = 20;
|
||||
|
||||
private readonly BattleAmbientContext _ctx = new() {
|
||||
ViewerId = EngineGlobalInit.ThisViewerId,
|
||||
IsForecast = true,
|
||||
IsRandomDraw = true,
|
||||
};
|
||||
|
||||
private HeadlessNetworkBattleMgr? _mgr;
|
||||
private NetworkBattleReceiver? _receiver;
|
||||
|
||||
@@ -66,7 +74,10 @@ internal sealed class SessionBattleEngine
|
||||
public void Setup(int masterSeed,
|
||||
IReadOnlyList<long> seatADeck, IReadOnlyList<long> seatBDeck,
|
||||
int seatAClass = 1, int seatBClass = 2)
|
||||
=> SetupInternal(masterSeed, seatADeck, seatBDeck, seatAClass, seatBClass, rng: null);
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
SetupInternal(masterSeed, seatADeck, seatBDeck, seatAClass, seatBClass, rng: null);
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG SEAM (Phase 4 Option-A viability PROBE — NOT a production fix). Identical to
|
||||
/// <see cref="Setup(int, IReadOnlyList{long}, IReadOnlyList{long}, int, int)"/> but installs a logging
|
||||
@@ -78,6 +89,7 @@ internal sealed class SessionBattleEngine
|
||||
IReadOnlyList<long> seatADeck, IReadOnlyList<long> seatBDeck,
|
||||
int seatAClass = 1, int seatBClass = 2)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
var log = new List<RollEntry>();
|
||||
// The logger needs the mgr to read seat signals at roll time; the mgr is built inside Setup, so the
|
||||
// logger reads it lazily via a closure populated right after construction.
|
||||
@@ -153,6 +165,7 @@ internal sealed class SessionBattleEngine
|
||||
WireMulliganPhase(mgr); // wire OperateReceive.OnReceiveDeal -> StartDeal (deal seats the hand)
|
||||
|
||||
_mgr = mgr;
|
||||
_ctx.Mgr = _mgr;
|
||||
// Use the mgr's OWN receiver — the ctor already wired it to the mgr's OperateReceive +
|
||||
// NetworkBattleData (NetworkBattleManagerBase.cs:266, non-recovery branch). This is the same
|
||||
// receiver the engine's RecoveryDataHandler drives when replaying recorded frames.
|
||||
@@ -164,6 +177,7 @@ internal sealed class SessionBattleEngine
|
||||
/// returned as a detected-desync EVENT (ND6), never silently absorbed.</summary>
|
||||
public EngineIngestResult Receive(MsgEnvelope env, bool isPlayerSeat)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null || _receiver is null)
|
||||
throw new InvalidOperationException("Receive before Setup.");
|
||||
|
||||
@@ -333,76 +347,94 @@ internal sealed class SessionBattleEngine
|
||||
// when the engine isn't owned (single-active-engine gate), so a non-engine session never
|
||||
// crashes. Production handlers read ONLY that band.
|
||||
|
||||
public int LeaderLife(bool playerSeat) => Seat(playerSeat).Class.Life;
|
||||
public int Pp(bool playerSeat) => Seat(playerSeat).Pp;
|
||||
public int HandCount(bool playerSeat) => Seat(playerSeat).HandCardList.Count;
|
||||
public int DeckCount(bool playerSeat) => Seat(playerSeat).DeckCardList.Count;
|
||||
public int Turn(bool playerSeat) => Seat(playerSeat).Turn;
|
||||
public int LeaderLife(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).Class.Life; }
|
||||
public int Pp(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).Pp; }
|
||||
public int HandCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).HandCardList.Count; }
|
||||
public int DeckCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).DeckCardList.Count; }
|
||||
public int Turn(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).Turn; }
|
||||
|
||||
/// <summary>Followers in play, excluding the leader (the Class card occupies one slot of
|
||||
/// ClassAndInPlayCardList).</summary>
|
||||
public int BoardCount(bool playerSeat) => Math.Max(0, Seat(playerSeat).ClassAndInPlayCardList.Count - 1);
|
||||
public int BoardCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Math.Max(0, Seat(playerSeat).ClassAndInPlayCardList.Count - 1); }
|
||||
|
||||
/// <summary>The engine <c>Index</c> of the hand card at the given hand position. The receive-path
|
||||
/// Play frame addresses a card by its engine Index (playIdx), which equals deck position + 1 for
|
||||
/// a card dealt from the seeded deck.</summary>
|
||||
public int HandCardIndex(bool playerSeat, int handPos) => Seat(playerSeat).HandCardList[handPos].Index;
|
||||
public int HandCardIndex(bool playerSeat, int handPos) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).HandCardList[handPos].Index; }
|
||||
|
||||
/// <summary>The real <c>CardId</c> (wire identity) of the hand card at <paramref name="handPos"/>. Lets a
|
||||
/// test locate a specific card in a SHUFFLED opening hand by identity (then read its <see cref="HandCardIndex"/>
|
||||
/// to drive a play), without depending on which shuffled position the card landed at.</summary>
|
||||
public int HandCardId(bool playerSeat, int handPos) => Seat(playerSeat).HandCardList[handPos].CardId;
|
||||
public int HandCardId(bool playerSeat, int handPos) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).HandCardList[handPos].CardId; }
|
||||
|
||||
/// <summary>The real <c>CardId</c> (wire identity) of the in-play follower at <paramref name="boardPos"/>
|
||||
/// (0-based, skipping the leader/Class card at ClassAndInPlayCardList[0] — same convention as
|
||||
/// <see cref="BoardCount"/>). Used to assert an opponent reveal seated the substituted card with its
|
||||
/// true identity (M-HC-2): before the reveal the slot holds a hidden dummy (cardId 0); after, the
|
||||
/// engine-resolved actual card carries the wire cardId.</summary>
|
||||
public int InPlayCardId(bool playerSeat, int boardPos) =>
|
||||
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].CardId;
|
||||
public int InPlayCardId(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].CardId;
|
||||
}
|
||||
|
||||
/// <summary>The engine <c>Index</c> of the in-play follower at <paramref name="boardPos"/> (0-based,
|
||||
/// leader excluded — same convention as <see cref="BoardCount"/>/<see cref="InPlayCardId"/>). An ATTACK
|
||||
/// frame addresses the attacker by this in-play Index (the wire <c>playIdx</c>), so a test reads it after
|
||||
/// a follower resolves onto the board to build the attack (M-HC-4a).</summary>
|
||||
public int InPlayCardIndex(bool playerSeat, int boardPos) =>
|
||||
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Index;
|
||||
public int InPlayCardIndex(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Index;
|
||||
}
|
||||
|
||||
/// <summary>The current life/health of the in-play follower at <paramref name="boardPos"/> (0-based,
|
||||
/// leader excluded). Reads <see cref="BattleCardBase.Life"/> (skill-resolved current health). Lets an
|
||||
/// attack test assert a follower took the attacker's damage (M-HC-4a follower-vs-follower trade).</summary>
|
||||
public int InPlayCardLife(bool playerSeat, int boardPos) =>
|
||||
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Life;
|
||||
public int InPlayCardLife(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Life;
|
||||
}
|
||||
|
||||
/// <summary>The attack stat of the in-play follower at <paramref name="boardPos"/> (skill-resolved
|
||||
/// <see cref="BattleCardBase.Atk"/>). The damage it deals when it attacks.</summary>
|
||||
public int InPlayCardAtk(bool playerSeat, int boardPos) =>
|
||||
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Atk;
|
||||
public int InPlayCardAtk(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Atk;
|
||||
}
|
||||
|
||||
/// <summary>True when the in-play follower at <paramref name="boardPos"/> can still attack this turn
|
||||
/// (<see cref="BattleCardBase.Attackable"/>). After it attacks (consuming its single attack) this reads
|
||||
/// false — the "attacker is spent" assertion (M-HC-4a).</summary>
|
||||
public bool InPlayCardAttackable(bool playerSeat, int boardPos) =>
|
||||
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Attackable;
|
||||
public bool InPlayCardAttackable(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Attackable;
|
||||
}
|
||||
|
||||
/// <summary>True once the in-play follower at <paramref name="boardPos"/> (0-based, leader excluded)
|
||||
/// has evolved (<see cref="UnitBattleCard.IsEvolution"/>, set true inside the engine's own
|
||||
/// <c>UnitBattleCard.Evolution</c> mutation). Only <see cref="UnitBattleCard"/> followers carry the
|
||||
/// flag; a non-follower (or the leader) reads false. The evolve test's decisive engine-state assertion
|
||||
/// (M-HC-4b).</summary>
|
||||
public bool IsEvolved(bool playerSeat, int boardPos) =>
|
||||
(Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1] as UnitBattleCard)?.IsEvolution ?? false;
|
||||
public bool IsEvolved(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return (Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1] as UnitBattleCard)?.IsEvolution ?? false;
|
||||
}
|
||||
|
||||
/// <summary>The seat's current evolve-point count (<see cref="BattlePlayerBase.CurrentEpCount"/>). An
|
||||
/// evolve spends one EP, so the evolve test asserts this decrements by 1. EP is granted at setup by
|
||||
/// the engine's <c>SetupEvolCount</c> (2 for the game-first seat, 3 for the second) and unlocks once
|
||||
/// <c>EvolveWaitTurnCount</c> has counted down (M-HC-4b).</summary>
|
||||
public int EpCount(bool playerSeat) => Seat(playerSeat).CurrentEpCount;
|
||||
public int EpCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).CurrentEpCount; }
|
||||
|
||||
/// <summary>Turns remaining until <paramref name="playerSeat"/> may evolve
|
||||
/// (<see cref="BattlePlayerBase.EvolveWaitTurnCount"/>); 0 means evolve is unlocked. Lets a test ramp to
|
||||
/// the evolve-enabled turn deterministically (M-HC-4b).</summary>
|
||||
public int EvolveWaitTurnCount(bool playerSeat) => Seat(playerSeat).EvolveWaitTurnCount;
|
||||
public int EvolveWaitTurnCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).EvolveWaitTurnCount; }
|
||||
|
||||
/// <summary>The engine-RESOLVED play-time cost of the card whose engine <c>Index</c> == <paramref name="idx"/>
|
||||
/// on <paramref name="playerSeat"/> (M-HC-3a). This is the discounted cost the play actually paid —
|
||||
@@ -422,6 +454,7 @@ internal sealed class SessionBattleEngine
|
||||
/// session never crashes and a vanilla play simply emits its base cost via the caller's fallback.</para></summary>
|
||||
public int PlayedCardCost(bool playerSeat, int idx, int fallback = 0)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
if (card is null) return fallback;
|
||||
@@ -447,6 +480,7 @@ internal sealed class SessionBattleEngine
|
||||
/// card — so a non-engine session never crashes and a vanilla play emits 0 via the caller's fallback.</para></summary>
|
||||
public int PlayedCardSpellboost(bool playerSeat, int idx, int fallback = 0)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
return card?.SpellChargeCount ?? fallback;
|
||||
@@ -470,6 +504,7 @@ internal sealed class SessionBattleEngine
|
||||
/// the caller's fallback, never crashing.</summary>
|
||||
public long PlayedCardId(bool playerSeat, int idx, long fallback = 0)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
return card is null ? fallback : card.CardId;
|
||||
@@ -485,6 +520,7 @@ internal sealed class SessionBattleEngine
|
||||
/// <see cref="PlayedCardCost"/>: no engine / no card → fallback, so a non-engine session never crashes.</para></summary>
|
||||
public int PlayedCardClan(bool playerSeat, int idx, int fallback = 0)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
return card is null ? fallback : (int)card.Clan;
|
||||
@@ -508,6 +544,7 @@ internal sealed class SessionBattleEngine
|
||||
/// legal wire value.</para></summary>
|
||||
public string PlayedCardTribe(bool playerSeat, int idx, string fallback = "0")
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
if (card is null) return fallback;
|
||||
@@ -545,6 +582,7 @@ internal sealed class SessionBattleEngine
|
||||
/// No-op-returns -1 if the engine isn't set up or no hand card has that Index.</summary>
|
||||
internal int SeedHandCardSpellboostCost(bool playerSeat, int idx, int charge)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return -1;
|
||||
BattleCardBase? card = null;
|
||||
foreach (var c in Seat(playerSeat).HandCardList)
|
||||
@@ -566,14 +604,21 @@ internal sealed class SessionBattleEngine
|
||||
/// .IsActive</c>, BattlePlayerBase.cs:3049/3073). Under the live recovery setup
|
||||
/// (<c>CreateXorShift(-1,-1)</c> via NullRecoveryManager.IdxChangeSeed == -1) this is FALSE, so the
|
||||
/// engine SKIPS the reshuffle the real clients performed.</summary>
|
||||
internal bool SelfXorShiftActive => (_mgr?.XorShiftRandom(isSelf: true)?.IsActive) ?? false;
|
||||
internal bool SelfXorShiftActive
|
||||
{
|
||||
get { using var _ambient = BattleAmbient.Enter(_ctx); return (_mgr?.XorShiftRandom(isSelf: true)?.IsActive) ?? false; }
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG: same as <see cref="SelfXorShiftActive"/> for the OPPONENT seat.</summary>
|
||||
internal bool OppoXorShiftActive => (_mgr?.XorShiftRandom(isSelf: false)?.IsActive) ?? false;
|
||||
internal bool OppoXorShiftActive
|
||||
{
|
||||
get { using var _ambient = BattleAmbient.Enter(_ctx); return (_mgr?.XorShiftRandom(isSelf: false)?.IsActive) ?? false; }
|
||||
}
|
||||
|
||||
/// <summary>DIAGNOSTIC: check if OnReceiveDeal is wired and report deck/hand counts.</summary>
|
||||
internal string DiagnoseDealState()
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) return "mgr=null";
|
||||
var or = _mgr.OperateReceive;
|
||||
bool dealWired = or.OnReceiveDeal != null;
|
||||
@@ -591,6 +636,7 @@ internal sealed class SessionBattleEngine
|
||||
/// after feeding the Ready.</summary>
|
||||
internal void SeedOppoIdxChange(int oppoSeed)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
_mgr?.CreateXorShift(-1, oppoSeed);
|
||||
}
|
||||
|
||||
@@ -599,6 +645,7 @@ internal sealed class SessionBattleEngine
|
||||
/// seed + <see cref="SeedOppoIdxChange"/> for the opponent seed.</summary>
|
||||
internal void DebugSeedIdxChange(int selfSeed, int oppoSeed)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) throw new InvalidOperationException("DebugSeedIdxChange before Setup.");
|
||||
_mgr.CreateXorShift(selfSeed, oppoSeed);
|
||||
}
|
||||
@@ -608,7 +655,11 @@ internal sealed class SessionBattleEngine
|
||||
/// <c>SetupInitialGameState(areCardsRandomlyDrawn:true)</c>). This seam exists so tests can
|
||||
/// force it false to reproduce the old top-of-deck bug. Static field → set per run under
|
||||
/// [NonParallelizable].</summary>
|
||||
internal void DebugSetRandomDraw(bool value) => BattleManagerBase.IsRandomDraw = value;
|
||||
internal void DebugSetRandomDraw(bool value)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
BattleManagerBase.IsRandomDraw = value;
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG (Phase 4 draw-recompute hypothesis): advance the SHARED <c>_stableRandom</c>
|
||||
/// stream by <paramref name="n"/> draws, exactly as <c>OperateReceive.StartOperate</c> does on a
|
||||
@@ -617,6 +668,7 @@ internal sealed class SessionBattleEngine
|
||||
/// is offset; this applies the pre-roll at the same point the real client would.</summary>
|
||||
internal void DebugSpinPreroll(int n)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) throw new InvalidOperationException("DebugSpinPreroll before Setup.");
|
||||
for (int i = 0; i < n; i++) _mgr.StableRandomDouble();
|
||||
}
|
||||
@@ -630,6 +682,7 @@ internal sealed class SessionBattleEngine
|
||||
/// InitBattleHandler.cs:28).</summary>
|
||||
internal double DebugStableRandomDouble()
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
if (_mgr is null) throw new InvalidOperationException("DebugStableRandomDouble before Setup.");
|
||||
return _mgr.StableRandomDouble();
|
||||
}
|
||||
@@ -640,17 +693,26 @@ internal sealed class SessionBattleEngine
|
||||
/// the real client's <c>SBattleLoad.InitPlayer</c> tail, SBattleLoad.cs:1292), so the FIRST
|
||||
/// generated token gets Index 41 — clear of deck-loaded indices 1..40 — and matches the wire
|
||||
/// <c>add.idx</c>. A stale value of 0 causes tokens to take Index 0, 1, ... and collide.</summary>
|
||||
internal int DebugCardTotalNum(bool playerSeat) =>
|
||||
_mgr is null ? -1 : _mgr.GetBattlePlayer(playerSeat).cardTotalNum;
|
||||
internal int DebugCardTotalNum(bool playerSeat)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return _mgr is null ? -1 : _mgr.GetBattlePlayer(playerSeat).cardTotalNum;
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG: the engine's running <c>StableRandom</c>/<c>StableRandomDouble</c> call count
|
||||
/// (private <c>BattleManagerBase.stableRandomCount</c>), so a divergence dump can report how far the
|
||||
/// shared stream has advanced at the moment of a mismatch.</summary>
|
||||
internal int DebugStableRandomCount =>
|
||||
_mgr is null ? -1
|
||||
: (int)(typeof(BattleManagerBase)
|
||||
.GetField("stableRandomCount", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(_mgr) ?? -1);
|
||||
internal int DebugStableRandomCount
|
||||
{
|
||||
get
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return _mgr is null ? -1
|
||||
: (int)(typeof(BattleManagerBase)
|
||||
.GetField("stableRandomCount", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(_mgr) ?? -1);
|
||||
}
|
||||
}
|
||||
|
||||
private engine::BattlePlayerBase Seat(bool playerSeat) =>
|
||||
(_mgr ?? throw new InvalidOperationException("read before Setup")).GetBattlePlayer(playerSeat);
|
||||
|
||||
Reference in New Issue
Block a user