engine cleanup passes 4-7 + multi-instancing ambient rip
Squashes 146 commits from battle-engine-extraction. Net: 2,045 files changed, +11,896 / -158,687 lines. Ships engine passes 4-7 (dead-code cull, view-layer stub, receive-path shrink) plus the Phase-5 AsyncLocal ambient deletion that turns concurrent battles into a type-system property rather than a scope contract. ## What landed **Passes 4-7 (chunks 1-34):** Extended the Phase-4 const-false collapse into a cascading cull across the skill graph, view layer, and receive-path periphery. Six mode flags (IsWatchBattle/IsReplayBattle/IsAdmin/IsAdminWatch/IsPuzzleQuest/ IsAINetwork) became `const false`, every guarded block deleted. Field*.cs subclass ctors + BackGroundBase + ObjectChecker culled to no-ops. Mulligan family reworked to take a mgr param through IMulliganMgr.InitMulligan. Emotion/Recovery/Resource clusters null-stubbed. Prediction/OperationSimulator/ skill filters converted from static ambient reads to per-mgr reads via SkillPrm.ownerCard.SelfBattlePlayer.BattleMgr / ins.BattleMgr / this.BattleMgr. **Phase-5 ambient rip (chunks 35-47):** Deleted BattleAmbient / BattleAmbient- Context / TestBattleScope in full. Every per-battle mutable slot now lives on the mgr instance itself: mgr.InstanceIsForecast / InstanceIsRandomDraw / InstanceRecoveryInfo / InstanceViewerId / InstanceNetworkAgent / GameMgr BattleManagerBase.GetIns() returns null unconditionally; the residual static flags + 3 façades (Certification.ViewerId, Data.BattleRecoveryInfo, ToolboxGame.RealTimeNetworkAgent) are null-tolerant defaults kept for the handful of engine-internal readers that still reference their types. Zero BattleAmbient references anywhere in engine + node + tests. Added pre-seeded GameMgr ctor overload threaded through the mgr chain (BattleManagerBase → SingleBattleMgr / NetworkBattleManagerBase → NetworkStandard- BattleMgr → HeadlessBattleMgr / HeadlessNetworkBattleMgr). Fixtures build a GameMgr, seed it via HeadlessEngineEnv.SeedCharaIds/SeedNetUser, and pass it to the mgr's ctor — no ambient reach. Node side (SVSim.BattleNode/SessionBattleEngine): _ctx replaced with a plain GameMgr field; 34 `using var _ambient = BattleAmbient.Enter(_ctx)` scope wraps ripped from every accessor and mutator; EngineGlobalInit.WirePerSessionGameMgr takes GameMgr as a param and runs from SessionBattleEngine.SetupInternal BEFORE mgr construction. Test side: TestBattleScope deleted; 18 fixture [SetUp]s migrated to `HeadlessEngineEnv.EnsureProcessGlobals()`; MultiInstanceEngineTests rewritten around per-mgr construction (GetIns() → null is the pinned invariant). ## Regression fixes - **chunk-48** (MulliganCtrl): chunk-35's `= null` stubs on card lookups broke the live receive-driven Deal path (BattlePlayerBase.DrawCard NRE'd downstream of NetworkPlayerMulliganCtrl.StartMulliganVfx). Restored the three lookups via `_battlePlayer.BattleMgr.GetBattleCardIdx`. Engine tests were satisfied by the WireMulliganPhase seam; unit tests exposed the live-path gap. ## Ship state - SVSim.BattleEngine.Tests: 56/56 pass, 2 skip - SVSim.UnitTests: 1554/1554 pass (was 1523/31-fail before chunk 48) - Solution build: 0 source warnings (40 pre-existing NU1902 MessagePack CVEs in SVSim.EmulatedEntrypoint, unrelated) - Sequential PVP smoke: verified live (two back-to-back battles, no regression on cleanup/spinup) - Concurrent PVP smoke: verified live Adds tools/engine-port/ClosureAnalyzer/ — the Roslyn transitive-type-closure analyzer needed to make future cascade cleanup safe (per feedback memory "Engine cleanup needs closure tool" from the 2026-06-28 pass-3 failure). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
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;
|
||||
@@ -18,7 +16,6 @@ using SBattleLoad = engine::SBattleLoad;
|
||||
using CardTemplate = engine::CardTemplate;
|
||||
using GameObject = engine::UnityEngine.GameObject;
|
||||
using RealTimeNetworkAgent = engine::RealTimeNetworkAgent;
|
||||
using Gungnir = engine::Gungnir;
|
||||
using NetworkNullLogger = engine::NetworkNullLogger;
|
||||
using ToolboxGame = engine::Wizard.ToolboxGame;
|
||||
using GameMgr = engine::GameMgr;
|
||||
@@ -49,20 +46,13 @@ internal sealed class SessionBattleEngine
|
||||
{
|
||||
private const int DefaultLeaderLife = 20;
|
||||
|
||||
private readonly BattleAmbientContext _ctx = new() {
|
||||
ViewerId = EngineGlobalInit.ThisViewerId,
|
||||
IsForecast = true,
|
||||
IsRandomDraw = true,
|
||||
// Per-session BattleRecoveryInfo: the receive-conductor deal path runs under IsRecovery
|
||||
// (set after mgr construction below) and reads Data.BattleRecoveryInfo.IsMulliganEnd in
|
||||
// MulliganMgrBase.StartDeal — null reads NRE. Each session owns its own no-op instance with
|
||||
// IsMulliganEnd=false (the default); GetUninitializedObject skips the JsonData ctor. Each
|
||||
// SessionBattleEngine carries its own ambient _ctx, so per-session isolation is by construction
|
||||
// (the EngineGlobalInit fallback only seeded once-per-process and silently fell over for the
|
||||
// second + later session that entered a fresh ambient — diagnosed Task 7).
|
||||
RecoveryInfo = (engine::Wizard.BattleRecoveryInfo)FormatterServices
|
||||
.GetUninitializedObject(typeof(engine::Wizard.BattleRecoveryInfo)),
|
||||
};
|
||||
// Phase-5 chunk 47: was `BattleAmbientContext _ctx` — an ambient-scoped bundle of
|
||||
// per-session state (GameMgr / ViewerId / IsForecast / IsRandomDraw / RecoveryInfo /
|
||||
// NetworkAgent) that engine code read via BattleAmbient.Current. The ambient is dead
|
||||
// (chunk 46 made GetIns() return null); everything that was on _ctx now lives on the mgr
|
||||
// instance directly. What remains is the per-session GameMgr, kept as a plain field so it
|
||||
// can be seeded (WirePerSessionGameMgr) BEFORE the mgr ctor reads it via the (cc, gm) overload.
|
||||
private readonly GameMgr _gameMgr = new();
|
||||
|
||||
private HeadlessNetworkBattleMgr? _mgr;
|
||||
private NetworkBattleReceiver? _receiver;
|
||||
@@ -77,16 +67,15 @@ internal sealed class SessionBattleEngine
|
||||
/// the <c>CardClass</c> int value); they select the leader's class via the all-8-class
|
||||
/// ClassCharacterList EngineGlobalInit installs (chara_id == class_id for 1..8). The 3-arg overload
|
||||
/// behavior is preserved by the defaults (1/2), matching the test-harness charaIds.
|
||||
/// <para>NOTE: GameMgr is now per-session via <see cref="BattleAmbientContext.GameMgr"/>; the leader
|
||||
/// chara ids are set on the SESSION's GameMgr (resolved through the ambient by
|
||||
/// <c>EngineGlobalInit.WirePerSessionGameMgr</c>), not on a process-wide singleton. This is the Task-7
|
||||
/// payoff: concurrent sessions each own their own GameMgr + engine state, so the historical
|
||||
/// single-active-engine gate (deleted EngineSessionGate) is no longer needed.</para></summary>
|
||||
/// <para>NOTE: GameMgr is per-session on the mgr instance (Phase-5 ambient rip, chunk 47); leader
|
||||
/// chara ids are set on the SESSION's <c>_gameMgr</c> (seeded pre-ctor by
|
||||
/// <c>EngineGlobalInit.WirePerSessionGameMgr</c>), not on a process-wide singleton. This is the
|
||||
/// multi-instancing payoff: concurrent sessions each own their own GameMgr + engine state, so the
|
||||
/// historical single-active-engine gate (deleted EngineSessionGate) is no longer needed.</para></summary>
|
||||
public void Setup(int masterSeed,
|
||||
IReadOnlyList<long> seatADeck, IReadOnlyList<long> seatBDeck,
|
||||
int seatAClass = 1, int seatBClass = 2)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
SetupInternal(masterSeed, seatADeck, seatBDeck, seatAClass, seatBClass, rng: null);
|
||||
}
|
||||
|
||||
@@ -100,7 +89,6 @@ 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.
|
||||
@@ -120,12 +108,29 @@ internal sealed class SessionBattleEngine
|
||||
// here rather than throwing into the shadow's no-op path (Phase 2 N2, carried-risk A).
|
||||
EngineGlobalInit.EnsureInitialized();
|
||||
|
||||
// Phase-5 chunk 46: seed the session's own GameMgr (chara ids + net-user) BEFORE mgr
|
||||
// construction so the base ctor's BattlePlayer/DataMgr reads resolve. Was piggybacked on
|
||||
// EnsureInitialized via ambient reach; now explicit and per-session.
|
||||
EngineGlobalInit.WirePerSessionGameMgr(_gameMgr);
|
||||
|
||||
// rng defaults to SeededRandomSource(masterSeed) inside the mgr — masterSeed here is the
|
||||
// engine's StableRandom seed (parameter name preserved for API compatibility; callers pass
|
||||
// BattleSeeds.Stable(rootMasterSeed) so the stream is born aligned with the seed the node
|
||||
// ships to both clients in Matched.seed). F-N-5; O-N-2 "bit-aligned anyway".
|
||||
var mgr = new HeadlessNetworkBattleMgr(new SessionContentsCreator(masterSeed), rng);
|
||||
// Phase-5 chunk 45: seed the mgr's GameMgr via _ctx (the session's ambient-attached GameMgr)
|
||||
// and hand it to the mgr's pre-seeded ctor. Eliminates the "ambient reaches into mgr ctor"
|
||||
// step; equivalent behavior since _gameMgr is the same instance the node reads later.
|
||||
var mgr = new HeadlessNetworkBattleMgr(new SessionContentsCreator(masterSeed), rng, _gameMgr);
|
||||
if (mgrBox is not null) mgrBox[0] = mgr; // publish for the test roll-logger closure (DebugSetupWithRollLog)
|
||||
|
||||
// Publish the mgr on the ambient EARLY (Phase 5a, 2026-07-02). Pre-Phase-5a the ambient
|
||||
// stored NetworkAgent/RecoveryInfo/IsForecast/etc. directly, so late-attach was fine.
|
||||
// Post-5a those fields are instance-backed on the mgr, and every accessor routes through
|
||||
// BattleManagerBase.GetIns() → ambient.Mgr — so any subsequent setup call that reaches for
|
||||
// one of them (InstallHeadlessNetworkAgent, SetGameMgrCharaIds, etc.) needs the mgr already
|
||||
// attached. The later `_mgr = mgr; // Phase-5 chunk 47: was _ctx.Mgr = _mgr — ambient publish is dead.` line is now a no-op re-assignment;
|
||||
// kept there for readability at the "wire mulligan" seam it originally guarded.
|
||||
// Phase-5 chunk 47: was _ctx.Mgr = mgr — ambient publish is dead (GetIns() returns null unconditionally).
|
||||
// Recovery mode is the engine's OWN headless replay path: the live view/UI touches on the
|
||||
// receive cycle (BattleUIContainer.DisableMenu, turn-control UI, card-view creation, VFX
|
||||
// waits) are all gated `!IsRecovery` (BattleUIContainer.cs:130, BattleManagerBase.cs:1499+),
|
||||
@@ -157,18 +162,17 @@ internal sealed class SessionBattleEngine
|
||||
// (BattleManagerBase.cs:1110), routing LotteryRandomDrawCard through seeded StableRandom
|
||||
// instead of top-of-deck. Without it the shadow draws DeckCardList[0] every time while
|
||||
// clients draw seeded-random — desynchronizing the hand and every downstream field.
|
||||
BattleManagerBase.IsRandomDraw = true;
|
||||
mgr.InstanceIsRandomDraw = true; // Phase-5 chunk 43: was BattleManagerBase.IsRandomDraw static
|
||||
|
||||
InitLeaderLife(mgr); // a 0-life leader reads as game-over and silently blocks plays
|
||||
InitCardTemplates(mgr); // play/draw resolution touches the (no-op) card view layer
|
||||
InitHeadlessViews(mgr); // turn/play cycle dereferences UI-container + emotion refs
|
||||
SeedBattleLogManager(); // per-frame filter cleanup reads BattleLogManager fusion lists
|
||||
InstallHeadlessNetworkAgent(); // turn-flow resolve reads ToolboxGame.RealTimeNetworkAgent
|
||||
InstallHeadlessNetworkAgent(mgr); // turn-flow resolve reads mgr.InstanceNetworkAgent
|
||||
|
||||
// Per-session leader class: chara_id == class_id for 1..8 in the all-8-class ClassCharacterList,
|
||||
// so writing the seats' class ordinals into the SESSION's GameMgr DataMgr (resolved through the
|
||||
// ambient — see Setup remarks) resolves each leader's correct class.
|
||||
SetGameMgrCharaIds(seatAClass, seatBClass);
|
||||
SetGameMgrCharaIds(mgr.GameMgr, seatAClass, seatBClass);
|
||||
|
||||
SeedDeck(mgr, seatADeck, isPlayer: true);
|
||||
SeedDeck(mgr, seatBDeck, isPlayer: false);
|
||||
@@ -179,7 +183,7 @@ internal sealed class SessionBattleEngine
|
||||
// resolve to null and NRE on the very next field read. Set ambient.Mgr here so the wiring
|
||||
// resolves the per-session mgr cleanly.
|
||||
_mgr = mgr;
|
||||
_ctx.Mgr = _mgr;
|
||||
// Phase-5 chunk 47: was _ctx.Mgr = _mgr — ambient publish is dead.
|
||||
|
||||
WireMulliganPhase(mgr); // wire OperateReceive.OnReceiveDeal -> StartDeal (deal seats the hand)
|
||||
|
||||
@@ -194,7 +198,6 @@ 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.");
|
||||
|
||||
@@ -364,25 +367,25 @@ internal sealed class SessionBattleEngine
|
||||
// when _mgr is null (Setup failed and the ComputeFrames try/catch swallowed it, ND6), so a
|
||||
// non-engine session never crashes. Production handlers read ONLY that band.
|
||||
|
||||
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; }
|
||||
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;
|
||||
|
||||
/// <summary>Followers in play, excluding the leader (the Class card occupies one slot of
|
||||
/// ClassAndInPlayCardList).</summary>
|
||||
public int BoardCount(bool playerSeat) { using var _ambient = BattleAmbient.Enter(_ctx); return Math.Max(0, Seat(playerSeat).ClassAndInPlayCardList.Count - 1); }
|
||||
public int BoardCount(bool playerSeat) => 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) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).HandCardList[handPos].Index; }
|
||||
public int HandCardIndex(bool playerSeat, int handPos) => 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) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).HandCardList[handPos].CardId; }
|
||||
public int HandCardId(bool playerSeat, int handPos) => 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
|
||||
@@ -391,7 +394,6 @@ internal sealed class SessionBattleEngine
|
||||
/// engine-resolved actual card carries the wire cardId.</summary>
|
||||
public int InPlayCardId(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].CardId;
|
||||
}
|
||||
|
||||
@@ -401,7 +403,6 @@ internal sealed class SessionBattleEngine
|
||||
/// a follower resolves onto the board to build the attack (M-HC-4a).</summary>
|
||||
public int InPlayCardIndex(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Index;
|
||||
}
|
||||
|
||||
@@ -410,7 +411,6 @@ internal sealed class SessionBattleEngine
|
||||
/// 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)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Life;
|
||||
}
|
||||
|
||||
@@ -418,7 +418,6 @@ internal sealed class SessionBattleEngine
|
||||
/// <see cref="BattleCardBase.Atk"/>). The damage it deals when it attacks.</summary>
|
||||
public int InPlayCardAtk(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Atk;
|
||||
}
|
||||
|
||||
@@ -427,7 +426,6 @@ internal sealed class SessionBattleEngine
|
||||
/// false — the "attacker is spent" assertion (M-HC-4a).</summary>
|
||||
public bool InPlayCardAttackable(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Attackable;
|
||||
}
|
||||
|
||||
@@ -438,7 +436,6 @@ internal sealed class SessionBattleEngine
|
||||
/// (M-HC-4b).</summary>
|
||||
public bool IsEvolved(bool playerSeat, int boardPos)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return (Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1] as UnitBattleCard)?.IsEvolution ?? false;
|
||||
}
|
||||
|
||||
@@ -446,12 +443,12 @@ internal sealed class SessionBattleEngine
|
||||
/// 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) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).CurrentEpCount; }
|
||||
public int EpCount(bool playerSeat) => 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) { using var _ambient = BattleAmbient.Enter(_ctx); return Seat(playerSeat).EvolveWaitTurnCount; }
|
||||
public int EvolveWaitTurnCount(bool playerSeat) => 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 —
|
||||
@@ -471,7 +468,6 @@ 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;
|
||||
@@ -497,7 +493,6 @@ 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;
|
||||
@@ -521,7 +516,6 @@ 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;
|
||||
@@ -537,7 +531,6 @@ 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;
|
||||
@@ -561,7 +554,6 @@ internal sealed class SessionBattleEngine
|
||||
/// entry), so this path must hand back a 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;
|
||||
@@ -599,7 +591,6 @@ 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)
|
||||
@@ -623,19 +614,18 @@ internal sealed class SessionBattleEngine
|
||||
/// engine SKIPS the reshuffle the real clients performed.</summary>
|
||||
internal bool SelfXorShiftActive
|
||||
{
|
||||
get { using var _ambient = BattleAmbient.Enter(_ctx); return (_mgr?.XorShiftRandom(isSelf: true)?.IsActive) ?? false; }
|
||||
get => (_mgr?.XorShiftRandom(isSelf: true)?.IsActive) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG: same as <see cref="SelfXorShiftActive"/> for the OPPONENT seat.</summary>
|
||||
internal bool OppoXorShiftActive
|
||||
{
|
||||
get { using var _ambient = BattleAmbient.Enter(_ctx); return (_mgr?.XorShiftRandom(isSelf: false)?.IsActive) ?? false; }
|
||||
get => (_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;
|
||||
@@ -653,7 +643,6 @@ internal sealed class SessionBattleEngine
|
||||
/// after feeding the Ready.</summary>
|
||||
internal void SeedOppoIdxChange(int oppoSeed)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
_mgr?.CreateXorShift(-1, oppoSeed);
|
||||
}
|
||||
|
||||
@@ -662,7 +651,6 @@ 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);
|
||||
}
|
||||
@@ -674,8 +662,9 @@ internal sealed class SessionBattleEngine
|
||||
/// [NonParallelizable].</summary>
|
||||
internal void DebugSetRandomDraw(bool value)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
BattleManagerBase.IsRandomDraw = value;
|
||||
// Phase-5 chunk 43: was ambient-scoped BattleManagerBase.IsRandomDraw = value; now
|
||||
// writes the mgr instance directly. _mgr is the session's authoritative mgr.
|
||||
if (_mgr is not null) _mgr.InstanceIsRandomDraw = value;
|
||||
}
|
||||
|
||||
/// <summary>TEST/DEBUG (Phase 4 draw-recompute hypothesis): advance the SHARED <c>_stableRandom</c>
|
||||
@@ -685,7 +674,6 @@ 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();
|
||||
}
|
||||
@@ -699,7 +687,6 @@ 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();
|
||||
}
|
||||
@@ -712,7 +699,6 @@ internal sealed class SessionBattleEngine
|
||||
/// <c>add.idx</c>. A stale value of 0 causes tokens to take Index 0, 1, ... and collide.</summary>
|
||||
internal int DebugCardTotalNum(bool playerSeat)
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return _mgr is null ? -1 : _mgr.GetBattlePlayer(playerSeat).cardTotalNum;
|
||||
}
|
||||
|
||||
@@ -723,8 +709,7 @@ internal sealed class SessionBattleEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
using var _ambient = BattleAmbient.Enter(_ctx);
|
||||
return _mgr is null ? -1
|
||||
return _mgr is null ? -1
|
||||
: (int)(typeof(BattleManagerBase)
|
||||
.GetField("stableRandomCount", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(_mgr) ?? -1);
|
||||
@@ -832,7 +817,7 @@ internal sealed class SessionBattleEngine
|
||||
// MulliganInfoControl. Node seed (allowed); the control is never shown/updated headless.
|
||||
var prefab = new GameObject();
|
||||
SeedMulliganInfoControl(prefab);
|
||||
var prefabData = GameMgr.GetIns().GetPrefabMgr().GetPrefabData();
|
||||
var prefabData = mgr.GameMgr.GetPrefabMgr().GetPrefabData();
|
||||
prefabData["Prefab/UI/MulliganInfo"] = prefab;
|
||||
|
||||
var phase = new NetworkMulliganPhase(mgr, mgr.NetworkSender);
|
||||
@@ -910,44 +895,30 @@ internal sealed class SessionBattleEngine
|
||||
return card;
|
||||
}
|
||||
|
||||
// The per-frame skill-filter cleanup (BattleManagerBase.RemoveUnUseCalledFilterDictionary, run on
|
||||
// EVERY receive) reads BattleLogManager.GetInstance().EnemyFusionCard.Contains(...) when a card with a
|
||||
// registered CalledCreateFilter is alive — e.g. a follower with a when_play spell_charge/fanfare skill
|
||||
// (BattleManagerBase.cs:155). The shim BattleLogManager singleton leaves PlayerFusionCard/EnemyFusionCard
|
||||
// null (no UI ran SetUp), so that .Contains NREs. Seed both to empty lists — a pure no-op view-state
|
||||
// seed (the fusion log is cosmetic; nothing headless adds to it). Process-global like the other seeds.
|
||||
private static void SeedBattleLogManager()
|
||||
{
|
||||
var log = BattleLogManager.GetInstance();
|
||||
log.PlayerFusionCard ??= new List<BattleCardBase>();
|
||||
log.EnemyFusionCard ??= new List<BattleCardBase>();
|
||||
}
|
||||
|
||||
// The turn-flow + emit bookkeeping reads the global ToolboxGame.RealTimeNetworkAgent (e.g.
|
||||
// RealTimeNetworkAgent.GetIsFirstPlayer/GetTurnState, which delegate to GameMgr's
|
||||
// NetworkUserInfoData.TurnState; AddActionSequence touches _gungnir). Headless there is no socket
|
||||
// agent, so seed a no-op one — mirroring HeadlessFixture.NewNetworkEmitBattle. _notEmit short-
|
||||
// circuits the byte-push before any socket I/O; the shadow engine never originates a send anyway.
|
||||
// NetworkUserInfoData.TurnState). Headless there is no socket agent, so seed a no-op one —
|
||||
// mirroring HeadlessFixture.NewNetworkEmitBattle. Since the engine RTA is now a stub with
|
||||
// no-op method bodies (pass-7 engine cleanup), the historical internal seeds (_gungnir field,
|
||||
// _notEmit short-circuit) are no longer needed — the stub can't NRE inside its own methods.
|
||||
// NOTE: this is a process-global; one engine per process is assumed for the shadow (revisit for
|
||||
// live multi-session — see design O-N status). Idempotent enough for the per-battle setup.
|
||||
private static void InstallHeadlessNetworkAgent()
|
||||
private static void InstallHeadlessNetworkAgent(HeadlessNetworkBattleMgr mgr)
|
||||
{
|
||||
var agent = (RealTimeNetworkAgent)FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
|
||||
agent.SetCurrentMatchingStatus(RealTimeNetworkAgent.MatchingStatus.Prepared);
|
||||
SetField(agent, "_gungnir", FormatterServices.GetUninitializedObject(typeof(Gungnir)));
|
||||
SetProperty(agent, "NetworkLogger", new NetworkNullLogger());
|
||||
SetField(agent, "_notEmit", true);
|
||||
ToolboxGame.SetRealTimeNetworkBattle(agent);
|
||||
mgr.InstanceNetworkAgent = agent; // Phase-5 chunk 41: was ToolboxGame.SetRealTimeNetworkBattle(agent)
|
||||
}
|
||||
|
||||
// Write the two seats' class ordinals into the SESSION's GameMgr DataMgr leader chara ids. Mirrors
|
||||
// the test seam HeadlessFixture.cs:202-204 (SetField(dm, "_playerCharaId"/"_enemyCharaId", ...)).
|
||||
// chara_id == class_id for 1..8 in EngineGlobalInit's all-8-class ClassCharacterList, so the ordinal
|
||||
// selects the class. A non-positive ordinal (e.g. CardClass.None == 0) clamps to the default seat
|
||||
// (1/2). GameMgr is per-session (BattleAmbientContext.GameMgr); writes resolve through the ambient.
|
||||
private static void SetGameMgrCharaIds(int a, int b)
|
||||
// the test seam HeadlessFixture.cs (SetField(dm, "_playerCharaId"/"_enemyCharaId", ...)). chara_id
|
||||
// == class_id for 1..8 in EngineGlobalInit's all-8-class ClassCharacterList, so the ordinal selects
|
||||
// the class. A non-positive ordinal (e.g. CardClass.None == 0) clamps to the default seat (1/2).
|
||||
// GameMgr is per-session (mgr.GameMgr, seeded pre-ctor from _gameMgr).
|
||||
private static void SetGameMgrCharaIds(GameMgr gm, int a, int b)
|
||||
{
|
||||
var dm = GameMgr.GetIns().GetDataMgr();
|
||||
var dm = gm.GetDataMgr();
|
||||
SetField(dm, "_playerCharaId", a <= 0 ? 1 : a);
|
||||
SetField(dm, "_enemyCharaId", b <= 0 ? 2 : b);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user