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