Files
SVSimServer/SVSim.BattleEngine/Engine/NetworkUserInfoData.cs
gamer147 2d9a6eea4b 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>
2026-07-03 19:18:54 -04:00

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;
}
}