Files
SVSimServer/SVSim.BattleEngine/Engine/Wizard.Battle.Mulligan/MulliganCtrl.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

256 lines
8.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using Wizard.Battle.View.Vfx;
// TODO(engine-cleanup-pass2): 7 of 19 methods unrun in baseline
// Type: Wizard.Battle.Mulligan.MulliganCtrl
// See data_dumps/reports/engine-cleanup/live-methods.baseline.txt
namespace Wizard.Battle.Mulligan;
public abstract class MulliganCtrl
{
protected BattlePlayerBase _battlePlayer;
protected MulliganViewBase _mulliganView;
protected List<BattleCardBase> _firstDrawList = new List<BattleCardBase>(3);
protected List<BattleCardBase> _stockList = new List<BattleCardBase>(3);
protected List<int> _mulliganAfterCardIndexList;
protected int _mulliganChangedNum = -1;
public List<int> DealIdxList = new List<int>();
public MulliganCtrl(BattlePlayerBase player)
{
_battlePlayer = player;
}
public abstract VfxBase StartMulliganVfx(SkillProcessor skillProcessor);
public abstract VfxBase SubmitMulliganVfx(IList<BattleCardBase> abandonCards);
protected VfxBase _MulliganCardChange(IList<BattleCardBase> AbandonCards)
{
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
if (AbandonCards.Count > 0)
{
IDictionary<int, BattleCardBase> newList = _MoveNewCardToHand(AbandonCards);
_ReturnAbandonToDeck(AbandonCards);
sequentialVfxPlayer.Register(_MulliganSwap(newList, AbandonCards));
}
return sequentialVfxPlayer;
}
protected void _ReturnAbandonToDeck(IList<BattleCardBase> AbandonCards)
{
List<BattleCardBase> list = AbandonCards.Where((BattleCardBase c) => c != null).ToList();
for (int num = 0; num < list.Count(); num++)
{
GetBattlePlayer().AddToDeck(list[num]);
}
}
protected virtual IDictionary<int, BattleCardBase> _MoveNewCardToHand(IList<BattleCardBase> AbandonCards)
{
List<BattleCardBase> list = _stockList.Take(AbandonCards.Count).ToList();
_stockList.RemoveRange(0, AbandonCards.Count);
BattlePlayerBase player = GetBattlePlayer();
for (int i = 0; i < AbandonCards.Count; i++)
{
player.DeckCardList.Remove(list[i]);
int index = player.HandCardList.IndexOf(AbandonCards[i]);
player.HandCardList[index] = list[i];
}
return list.ToDictionary((BattleCardBase card) => player.HandCardList.IndexOf(card));
}
protected IDictionary<int, BattleCardBase> NetworkMoveNewCardToHand(IList<BattleCardBase> AbandonCards)
{
// Phase-5 chunk 48: restored card lookup — chunk 35's stub broke live receive Deal path.
var battleMgr = _battlePlayer.BattleMgr;
BattlePlayerBase player = GetBattlePlayer();
List<BattleCardBase> list = new List<BattleCardBase>();
for (int i = 0; i < _mulliganAfterCardIndexList.Count; i++)
{
int num = _mulliganAfterCardIndexList[i];
if (!DealIdxList.Contains(num))
{
list.Add(battleMgr.GetBattleCardIdx(player.DeckCardList, num));
}
}
if (AbandonCards.Count != list.Count)
{
string text = "";
for (int j = 0; j < AbandonCards.Count; j++)
{
if (j > 0)
{
text += ",";
}
text += AbandonCards[j].Index;
}
string text2 = "";
for (int k = 0; k < list.Count; k++)
{
if (k > 0)
{
text2 += ",";
}
text2 += list[k].Index;
}
throw new Exception($"Card swap failedAbandonCards【{text}】/DrawCards【{text2}】");
}
SortedList<int, BattleCardBase> sortedList = new SortedList<int, BattleCardBase>();
for (int l = 0; l < AbandonCards.Count; l++)
{
int key = DealIdxList.IndexOf(AbandonCards[l].Index);
sortedList.Add(key, AbandonCards[l]);
}
IList<int> keys = sortedList.Keys;
for (int m = 0; m < keys.Count; m++)
{
int key2 = keys[m];
player.DeckCardList.Remove(list[m]);
int index = player.HandCardList.IndexOf(sortedList[key2]);
player.HandCardList[index] = list[m];
}
return list.ToDictionary((BattleCardBase card) => player.HandCardList.IndexOf(card));
}
protected abstract VfxBase _MulliganSwap(IDictionary<int, BattleCardBase> newList, IList<BattleCardBase> oldList);
protected VfxBase _CardSwapAndMoveToStaticPositionVfx(IDictionary<int, BattleCardBase> newList, IList<BattleCardBase> oldList, bool isCardHolderPlayer, bool isHideMulliganTitle)
{
List<int> list = newList.Keys.ToList();
List<BattleCardBase> list2 = newList.Values.ToList();
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
sequentialVfxPlayer.Register(InstantVfx.Create(delegate
{
_mulliganView.HideMulliganUIAbandonZone();
if (isHideMulliganTitle)
{
_mulliganView.HideMulliganTitle();
}
}));
sequentialVfxPlayer.Register(NullVfx.GetInstance());
int count = newList.Count;
ParallelVfxPlayer parallelVfxPlayer = ParallelVfxPlayer.Create();
for (int num = 0; num < count; num++)
{
BattleCardBase card = list2[num];
int posIndex = list[num];
parallelVfxPlayer.Register(_mulliganView.MoveCardToStaticPosition(card, posIndex, isAbandon: false));
}
parallelVfxPlayer.Register(InstantVfx.Create(delegate
{
}));
sequentialVfxPlayer.Register(parallelVfxPlayer);
return sequentialVfxPlayer;
}
public void GetAbandonCardList(BattleManagerBase battleMgr, ref List<BattleCardBase> retCardList, ref List<int> retPosList)
{
IEnumerable<int> source = DealIdxList.Where((int index) => !_mulliganAfterCardIndexList.Contains(index));
retPosList = source.Select((int index) => DealIdxList.IndexOf(index)).ToList();
retCardList = source.Select((int index) => battleMgr.GetBattleCardIdx(GetBattlePlayer().AllCards.ToList(), index)).ToList();
}
public VfxBase DrawFirstMulliganCard()
{
SkillProcessor skillProcessor = new SkillProcessor();
return GetBattlePlayer().DrawCards(_firstDrawList, skillProcessor, isOpen: false, isMulligan: true).Vfx;
}
protected List<int> _LotMulliganCardIndex(int maxNum)
{
List<int> list = new List<int>(6);
List<int> list2 = Enumerable.Range(1, maxNum).ToList();
if (_battlePlayer.BattleMgr.InstanceIsRandomDraw /* Pre-Phase-5b: IsNetworkBattle assumed true */)
{
for (int i = 0; i < 6; i++)
{
int index = 0; // Pre-Phase-5b: no mgr in scope; deterministic fallback
list.Add(list2[index]);
list2.Remove(list2[index]);
}
}
else
{
for (int j = 0; j < 6; j++)
{
list.Add(list2[j]);
}
}
return list;
}
protected void _CreateMulliganCardList(List<int> indexList)
{
// Phase-5 chunk 48 (2026-07-03): restored the real card lookup — chunk 35's null stub
// broke the live receive-driven Deal path (BattlePlayerBase.DrawCard NRE'd on null
// entries). mgr is reachable via _battlePlayer.BattleMgr (chunk 42 seam).
var battleMgr = _battlePlayer.BattleMgr;
List<BattleCardBase> deckCardList = GetBattlePlayer().DeckCardList;
for (int i = 0; i < 3; i++)
{
BattleCardBase battleCardIdx = battleMgr.GetBattleCardIdx(deckCardList, indexList[i]);
BattleCardBase battleCardIdx2 = battleMgr.GetBattleCardIdx(deckCardList, indexList[i + 3]);
_firstDrawList.Add(battleCardIdx);
_stockList.Add(battleCardIdx2);
}
}
public void CreateMulliganDealList(List<int> indexList)
{
// Phase-5 chunk 48 (2026-07-03): restored the real card lookup — see _CreateMulliganCardList.
var battleMgr = _battlePlayer.BattleMgr;
List<BattleCardBase> deckCardList = GetBattlePlayer().DeckCardList;
for (int i = 0; i < 3; i++)
{
BattleCardBase battleCardIdx = battleMgr.GetBattleCardIdx(deckCardList, indexList[i]);
_firstDrawList.Add(battleCardIdx);
}
}
public List<BattleCardBase> GetFirstDrawList()
{
return _firstDrawList;
}
public List<BattleCardBase> GetStockList()
{
return _stockList;
}
public List<int> GetMulliganAfterCardIndexList()
{
return _mulliganAfterCardIndexList;
}
public void SetMulliganAfterCardIndexList(List<int> indexList)
{
_mulliganAfterCardIndexList = indexList;
}
public int GetChangedNum()
{
return _mulliganChangedNum;
}
public BattlePlayerBase GetBattlePlayer()
{
return _battlePlayer;
}
public MulliganInfoControl GetMulliganInfo()
{
return _mulliganView.MulliganInfo;
}
}