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>
195 lines
5.3 KiB
C#
195 lines
5.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Wizard;
|
|
|
|
public class NetworkUserInfoData
|
|
{
|
|
public class NetworkUserInfo
|
|
{
|
|
public int Rank { get; private set; }
|
|
|
|
public bool IsMasterRank { get; private set; }
|
|
|
|
public int BattlePoint { get; private set; }
|
|
|
|
public int MasterPoint { get; private set; }
|
|
|
|
public int ClassId { get; private set; }
|
|
|
|
public int SubClassId { get; private set; } = 10;
|
|
|
|
public int CharaId { get; private set; }
|
|
|
|
public string MyRotationId { get; private set; } = "";
|
|
|
|
public string AvatarBattleId { get; private set; } = "";
|
|
|
|
public void SetParameter(Dictionary<string, object> info)
|
|
{
|
|
Rank = Convert.ToInt32(info["rank"]);
|
|
IsMasterRank = info.ContainsKey("isMasterRank") && Convert.ToInt32(info["isMasterRank"]) != 0;
|
|
BattlePoint = (info.ContainsKey("battlePoint") ? Convert.ToInt32(info["battlePoint"]) : 0);
|
|
MasterPoint = (info.ContainsKey("masterPoint") ? Convert.ToInt32(info["masterPoint"]) : 0);
|
|
ClassId = Convert.ToInt32(info["classId"]);
|
|
if (info.ContainsKey("subclassId"))
|
|
{
|
|
SubClassId = Convert.ToInt32(info["subclassId"]);
|
|
}
|
|
CharaId = Convert.ToInt32(info["charaId"]);
|
|
if (info.ContainsKey("rotationId"))
|
|
{
|
|
MyRotationId = Convert.ToString(info["rotationId"]);
|
|
}
|
|
AvatarBattleId = info["charaId"].ToString();
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, object> _selfInfo = new Dictionary<string, object>();
|
|
|
|
private Dictionary<string, object> _oppoInfo = new Dictionary<string, object>();
|
|
|
|
public int TurnState { get; set; }
|
|
|
|
public NetworkUserInfo SelfBattleStartInfo { get; private set; }
|
|
|
|
public NetworkUserInfo OppoBattleStartInfo { get; private set; }
|
|
|
|
public NetworkUserInfoData()
|
|
{
|
|
TurnState = -1;
|
|
}
|
|
|
|
public void SetSelfInfo(Dictionary<string, object> info, bool isWatchReplayRecovery)
|
|
{
|
|
_selfInfo = info;
|
|
if (isWatchReplayRecovery)
|
|
{
|
|
SetNetworkSelfInfo(info);
|
|
}
|
|
if (_selfInfo != null && _selfInfo.ContainsKey("seed"))
|
|
{
|
|
LocalLog.AccumulateLastTraceLog("SetSelfInfo seed" + Convert.ToInt32(_selfInfo["seed"]));
|
|
}
|
|
}
|
|
|
|
// SetNetworkSelfInfo used to fan the received chara/skin/rotation info out to the
|
|
// process-wide GameMgr.DataMgr (production-only path). In the current headless world
|
|
// the only call site is the isWatchReplayRecovery=true branch of SetSelfInfo, which
|
|
// is never taken in either test seeder — so this is dead in every code path we run.
|
|
// Body preserved as documentation until a live-network path revives it, but the
|
|
// GameMgr/Data reach has been dropped so it can't ambient-race anymore.
|
|
public void SetNetworkSelfInfo(Dictionary<string, object> info)
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
SelfBattleStartInfo = new NetworkUserInfo();
|
|
}
|
|
SelfBattleStartInfo.SetParameter(info);
|
|
// TODO(post-Phase-5b, revive-live-network): thread a mgr param through and fan out
|
|
// GetSelfCharaId/GetSelfSubClassId/GetSelfMyRotationId/GetSelfAvatarBattleId into
|
|
// mgr.GameMgr.GetDataMgr() + Data.RoomTwoPickBeforeBattleInfo once a caller exists.
|
|
if (_selfInfo != null && _selfInfo.ContainsKey("seed"))
|
|
{
|
|
LocalLog.AccumulateLastTraceLog("SetNetworkSelfInfo seed" + Convert.ToInt32(_selfInfo["seed"]));
|
|
}
|
|
}
|
|
|
|
public int GetFieldId()
|
|
{
|
|
return Convert.ToInt32(_selfInfo["fieldId"]);
|
|
}
|
|
|
|
public int GetRandomSeed()
|
|
{
|
|
if (_selfInfo == null || !_selfInfo.ContainsKey("seed"))
|
|
{
|
|
string text = "NotSeed ";
|
|
text = text + ((_selfInfo == null) ? "infoNull" : "noneKey") + " ";
|
|
if (_selfInfo != null)
|
|
{
|
|
foreach (KeyValuePair<string, object> item in _selfInfo)
|
|
{
|
|
text = text + item.Key + ":" + item.Value?.ToString() + " ";
|
|
}
|
|
}
|
|
LocalLog.AccumulateLastTraceLog(text);
|
|
return 0;
|
|
}
|
|
return Convert.ToInt32(_selfInfo["seed"]);
|
|
}
|
|
|
|
public int GetSelfViewerId()
|
|
{
|
|
return Convert.ToInt32(_selfInfo["viewerId"]);
|
|
}
|
|
|
|
public int GetSelfSubClassId()
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
return 0;
|
|
}
|
|
return SelfBattleStartInfo.SubClassId;
|
|
}
|
|
|
|
public int GetSelfCharaId()
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
return 0;
|
|
}
|
|
return SelfBattleStartInfo.CharaId;
|
|
}
|
|
|
|
public string GetSelfMyRotationId()
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
return "";
|
|
}
|
|
return SelfBattleStartInfo.MyRotationId;
|
|
}
|
|
|
|
public string GetSelfAvatarBattleId()
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
return "";
|
|
}
|
|
return SelfBattleStartInfo.AvatarBattleId;
|
|
}
|
|
|
|
public int GetSelfChaosId()
|
|
{
|
|
// The `!IsNetworkBattle` guard is redundant with the dict-lookup below: in single-player
|
|
// paths `_selfInfo` is empty (no network setup), so the dict never contains "chaosId"
|
|
// and this returns -1 anyway. Removing the ambient reach.
|
|
if (_selfInfo.ContainsKey("chaosId"))
|
|
{
|
|
return Convert.ToInt32(_selfInfo["chaosId"]);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public string GetOpponentAvatarBattleId()
|
|
{
|
|
if (SelfBattleStartInfo == null)
|
|
{
|
|
return "";
|
|
}
|
|
return OppoBattleStartInfo.AvatarBattleId;
|
|
}
|
|
|
|
public int GetOpponentChaosId()
|
|
{
|
|
// See GetSelfChaosId — same rationale, the `!IsNetworkBattle` guard is redundant with
|
|
// the dict-lookup fallthrough (single-player paths never populate `_oppoInfo`).
|
|
if (_oppoInfo.ContainsKey("chaosId"))
|
|
{
|
|
return Convert.ToInt32(_oppoInfo["chaosId"]);
|
|
}
|
|
return -1;
|
|
}
|
|
}
|