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:
@@ -3,6 +3,11 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- NU1903 (MessagePack 2.5.172 has a moderate-severity vulnerability) is the only
|
||||
audit hit at this revision. Suppress so MSBuildWorkspace (used by tools/engine-port/
|
||||
ClosureAnalyzer) doesn't bail out before wiring the extern-alias project reference,
|
||||
which silently breaks cross-project symbol resolution for this whole project. -->
|
||||
<NoWarn>$(NoWarn);NU1903</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
@@ -186,10 +186,10 @@ public sealed class BattleSession
|
||||
if (A is RealParticipant rpA) rpA.Outbound.Clear();
|
||||
if (B is RealParticipant rpB) rpB.Outbound.Clear();
|
||||
|
||||
// Per-session BattleAmbientContext on the engine isolates per-battle state across concurrent
|
||||
// sessions (Task 7 of multi-instancing migration), so the historical single-active-engine gate
|
||||
// (and its matching try/finally Release) is gone — engine setup is unconditional per session,
|
||||
// and there is no teardown obligation that must run on a throw from the participant tear-down.
|
||||
// Per-session mgr instance (Phase-5 ambient rip, chunk 47) isolates per-battle state across
|
||||
// concurrent sessions, so the historical single-active-engine gate (and its matching
|
||||
// try/finally Release) is gone — engine setup is unconditional per session, and there is no
|
||||
// teardown obligation that must run on a throw from the participant tear-down.
|
||||
await Task.WhenAll(
|
||||
A.TerminateAsync(BattleFinishReason.NormalFinish),
|
||||
B.TerminateAsync(BattleFinishReason.NormalFinish))
|
||||
@@ -278,10 +278,10 @@ public sealed class BattleSession
|
||||
{
|
||||
case NetworkBattleUri.Deal when !_engineDealFed:
|
||||
_engineDealFed = true;
|
||||
_log.LogWarning("BattleSession {Bid}: DEAL DIAG BEFORE: {Diag}",
|
||||
_log.LogDebug("BattleSession {Bid}: DEAL diag BEFORE: {Diag}",
|
||||
BattleId, _engine.DiagnoseDealState());
|
||||
ShadowFeed(frame, isPlayerSeat: true, "Deal");
|
||||
_log.LogWarning("BattleSession {Bid}: DEAL DIAG AFTER: {Diag}",
|
||||
_log.LogDebug("BattleSession {Bid}: DEAL diag AFTER: {Diag}",
|
||||
BattleId, _engine.DiagnoseDealState());
|
||||
break;
|
||||
|
||||
@@ -338,12 +338,12 @@ public sealed class BattleSession
|
||||
if (_engineSetupAttempted) return;
|
||||
_engineSetupAttempted = true;
|
||||
|
||||
// Per-session BattleAmbientContext on the engine isolates per-battle state across concurrent
|
||||
// sessions (Task 7 of multi-instancing migration), so the historical single-active-engine gate
|
||||
// (EngineSessionGate.TryAcquire) is gone and engine setup is unconditional. A genuine setup
|
||||
// failure still surfaces via ComputeFrames' shadow-engine try/catch (it logs + swallows so the
|
||||
// relay never sees an engine exception, ND6), and IsReady stays false in that case so
|
||||
// ShadowIngest/ShadowFeedServerFrames no-op for the rest of the battle.
|
||||
// Per-session mgr instance (Phase-5 ambient rip, chunk 47) isolates per-battle state across
|
||||
// concurrent sessions, so the historical single-active-engine gate (EngineSessionGate.TryAcquire)
|
||||
// is gone and engine setup is unconditional. A genuine setup failure still surfaces via
|
||||
// ComputeFrames' shadow-engine try/catch (it logs + swallows so the relay never sees an engine
|
||||
// exception, ND6), and IsReady stays false in that case so ShadowIngest/ShadowFeedServerFrames
|
||||
// no-op for the rest of the battle.
|
||||
//
|
||||
// Seed the engine's StableRandom with BattleSeeds.Stable(MasterSeed) — the SAME value the
|
||||
// Matched frame ships to both clients (InitBattleHandler.cs:28). The clients seed their
|
||||
@@ -362,6 +362,14 @@ public sealed class BattleSession
|
||||
private void ShadowIngest(IBattleParticipant from, MsgEnvelope env)
|
||||
{
|
||||
if (!_engine.IsReady) return;
|
||||
// Node-only handshake URIs (InitNetwork, InitBattle) have no counterpart on the engine's
|
||||
// NetworkBattleURI enum — they're pure relay plumbing (see InitNetworkHandler /
|
||||
// InitBattleHandler). Feeding them to the engine's MapUri throws ArgumentException that
|
||||
// the outer try/catch swallows, but produces a per-frame warn+stack-trace for every
|
||||
// battle setup. Skip at the ingest source rather than plumb an ignore-branch through the
|
||||
// engine (the whole point of the shadow being separate is that node protocol changes
|
||||
// shouldn't require engine touches).
|
||||
if (env.Uri is NetworkBattleUri.InitNetwork or NetworkBattleUri.InitBattle) return;
|
||||
bool isPlayerSeat = ReferenceEquals(from, A);
|
||||
var r = _engine.Receive(env, isPlayerSeat);
|
||||
if (r.Diverged)
|
||||
|
||||
@@ -60,11 +60,9 @@ 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();
|
||||
// Phase-5 chunk 46: per-session GameMgr wiring moved to SessionBattleEngine.SetupInternal
|
||||
// where the session's own GameMgr is in scope. EnsureInitialized only handles the true
|
||||
// process-globals below.
|
||||
|
||||
if (_done) return;
|
||||
lock (_gate)
|
||||
@@ -86,14 +84,15 @@ internal static class EngineGlobalInit
|
||||
.SetValue(null, new Crossover());
|
||||
}
|
||||
|
||||
// Suppress VFX / take the virtual-battle resolution path (no live view layer).
|
||||
BattleManagerBase.IsForecast = true;
|
||||
// Phase-5 chunk 43: was `BattleManagerBase.IsForecast = true` — a static ambient-bridge
|
||||
// write that ran before any mgr was constructed, so it stashed in ambient.IsForecast for
|
||||
// pre-Phase-5 propagation onto the mgr's InstanceIsForecast at attach. With chunk 42's
|
||||
// ambient rip that propagation is gone and the write became a silent no-op. Init here is
|
||||
// now redundant: BattleManagerBase.InstanceIsForecast = true by default.
|
||||
|
||||
// Post-Task-8: BattleRecoveryInfo lives on the per-session BattleAmbientContext (RecoveryInfo,
|
||||
// pre-seeded with an uninitialized no-op instance in SessionBattleEngine's field initializer).
|
||||
// Each session owns its own (IsMulliganEnd=false default), so the MulliganMgrBase.StartDeal
|
||||
// read resolves per-session — the historical process-global Data.BattleRecoveryInfo write that
|
||||
// lived here was dead the moment the ambient seam landed (reviewer-flagged in Task 7).
|
||||
// BattleRecoveryInfo lives on the mgr instance itself (InstanceRecoveryInfo). Historical
|
||||
// process-global write here was dead the moment the mgr's per-instance slot landed
|
||||
// (Task 7 / Phase-5a).
|
||||
|
||||
// --- static CardMaster (full cards.json) ----------------------------------------------
|
||||
// ALWAYS rebuild + re-inject the FULL master. We must not defer to a possibly-thin
|
||||
@@ -121,46 +120,31 @@ internal static class EngineGlobalInit
|
||||
udidField.SetValue(null, "headless-udid");
|
||||
|
||||
// --- Cute.Certification.viewer_id ------------------------------------------------------
|
||||
// Post-Task-8: viewer id lives on the per-session BattleAmbientContext (ViewerId, init-only).
|
||||
// SessionBattleEngine seeds it from ThisViewerId in its field initializer and enters the
|
||||
// ambient on every Setup/Receive — so the engine's Certification.ViewerId getter
|
||||
// (BattleAmbient.Require().ViewerId) always resolves cleanly when the engine code is
|
||||
// reached. The reflection write that used to seed `Certification._viewerIdFallback` is now
|
||||
// dead (the static is deleted). Left as a comment-only marker so the design intent (this
|
||||
// viewer id defines the engine's "player" perspective for the IsRecovery target parse)
|
||||
// stays discoverable from the global init path.
|
||||
// Viewer id lives on the mgr instance (InstanceViewerId, default 1001 = ThisViewerId).
|
||||
// Certification.ViewerId's static getter reads the mgr via GetIns()?.InstanceViewerId
|
||||
// — no seeding needed here.
|
||||
|
||||
_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
// Per-session GameMgr wiring: seeds DataMgr chara ids + NetworkUserInfoData on the session's own
|
||||
// GameMgr. Caller (SessionBattleEngine.SetupInternal) passes _gameMgr BEFORE mgr construction so
|
||||
// the base ctor's BattlePlayer/DataMgr reads resolve. Idempotent (SetFieldIfZeroOrNull no-ops once
|
||||
// set; netUser is null-guarded).
|
||||
public static void WirePerSessionGameMgr(GameMgr gm)
|
||||
{
|
||||
// --- 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();
|
||||
var dm = gm.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)
|
||||
if (gm.GetNetworkUserInfoData() == null)
|
||||
{
|
||||
var netUser = new NetworkUserInfoData();
|
||||
netUser.SetSelfInfo(
|
||||
new Dictionary<string, object> { ["fieldId"] = 1 },
|
||||
isWatchReplayRecovery: false);
|
||||
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
|
||||
gm.SetNetworkUserInfoData(netUser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -204,9 +204,23 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase
|
||||
{
|
||||
if (_diagnosticLogging)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"[ws-loop-exit] viewer={Vid} reason={Reason} wsState={State} cancelled={Cancelled}",
|
||||
ViewerId, exitReason, _ws.State, cancellation.IsCancellationRequested);
|
||||
// Clean-teardown terminators (normal client close, or a null read after
|
||||
// cancellation was requested) log at Info; anything else stays Warn so
|
||||
// genuine crashes stay visible even with diagnostics on.
|
||||
bool cleanExit = exitReason == "read-returned-null" &&
|
||||
(cancellation.IsCancellationRequested || _ws.State is WebSocketState.CloseReceived or WebSocketState.Closed);
|
||||
if (cleanExit)
|
||||
{
|
||||
_log.LogInformation(
|
||||
"[ws-loop-exit] viewer={Vid} reason={Reason} wsState={State} cancelled={Cancelled}",
|
||||
ViewerId, exitReason, _ws.State, cancellation.IsCancellationRequested);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.LogWarning(
|
||||
"[ws-loop-exit] viewer={Vid} reason={Reason} wsState={State} cancelled={Cancelled}",
|
||||
ViewerId, exitReason, _ws.State, cancellation.IsCancellationRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +509,14 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (_diagnosticLogging)
|
||||
_log.LogWarning("[ws-recv-exit] viewer={Vid} reason=OperationCanceled wsState={State}", ViewerId, _ws.State);
|
||||
{
|
||||
// A cancellation-requested cancel is a normal session teardown; a spontaneous
|
||||
// OperationCanceled without cancellation being requested is a real anomaly.
|
||||
if (ct.IsCancellationRequested)
|
||||
_log.LogInformation("[ws-recv-exit] viewer={Vid} reason=OperationCanceled wsState={State}", ViewerId, _ws.State);
|
||||
else
|
||||
_log.LogWarning("[ws-recv-exit] viewer={Vid} reason=OperationCanceled wsState={State}", ViewerId, _ws.State);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (WebSocketException wsex)
|
||||
@@ -508,8 +529,15 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
if (_diagnosticLogging)
|
||||
_log.LogWarning("[ws-recv-exit] viewer={Vid} reason=ClientCloseFrame wsState={State} closeStatus={Status} desc={Desc}",
|
||||
ViewerId, _ws.State, result.CloseStatus, result.CloseStatusDescription);
|
||||
{
|
||||
// NormalClosure ("Bye!") at battle-end is expected; abnormal close statuses stay Warn.
|
||||
if (result.CloseStatus == WebSocketCloseStatus.NormalClosure)
|
||||
_log.LogInformation("[ws-recv-exit] viewer={Vid} reason=ClientCloseFrame wsState={State} closeStatus={Status} desc={Desc}",
|
||||
ViewerId, _ws.State, result.CloseStatus, result.CloseStatusDescription);
|
||||
else
|
||||
_log.LogWarning("[ws-recv-exit] viewer={Vid} reason=ClientCloseFrame wsState={State} closeStatus={Status} desc={Desc}",
|
||||
ViewerId, _ws.State, result.CloseStatus, result.CloseStatusDescription);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
|
||||
Reference in New Issue
Block a user