extern alias engine;
using System.Reflection;
using System.Runtime.Serialization;
using engine::SVSim.BattleEngine.Rng;
using SVSim.BattleNode.Protocol;
using NetworkBattleReceiver = engine::NetworkBattleReceiver;
using NetworkBattleDefine = engine::NetworkBattleDefine;
using BattleManagerBase = engine::BattleManagerBase;
using BattlePlayerBase = engine::BattlePlayerBase;
using BattleCardBase = engine::BattleCardBase;
using ClassBattleCardBase = engine::ClassBattleCardBase;
using CardCreatorBase = engine::CardCreatorBase;
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;
using BattleUIContainer = engine::BattleUIContainer;
using BackGroundBase = engine::BackGroundBase;
using NullPlayerEmotion = engine::Wizard.Battle.Player.Emotion.NullPlayerEmotion;
namespace SVSim.BattleNode.Sessions.Engine;
/// One authoritative engine per BattleSession, seated as both players (design ND2). A faithful
/// SHADOW: it mirrors each client's resolved play, never overrides/rejects/originates (ND1). Ingest is
/// the engine's own NetworkBattleReceiver.ReceivedMessage (ND4); isPlayer selects the seat (F-N-2).
///
/// The headless wiring here is the production analogue of the test HeadlessFixture
/// (NewNetworkEmitBattle / SeedDeck / InitLeaderLife / InitCardTemplates). It deliberately omits the
/// emit-only RealTimeNetworkAgent scaffolding the test uses for the SEND path — the shadow engine only
/// RECEIVES (F-N-2), so no socket-agent is constructed. The engine's global init (CardMaster, GameMgr,
/// Wizard.Data) is the caller's responsibility (the test does HeadlessEngineEnv.EnsureInitialized;
/// the live node guards Setup in try/catch so an un-initialized host degrades to a no-op shadow).
internal sealed class SessionBattleEngine
{
private const int DefaultLeaderLife = 20;
private HeadlessNetworkBattleMgr? _mgr;
private NetworkBattleReceiver? _receiver;
/// True once Setup has built the two-seat battle.
public bool IsReady => _mgr is not null;
/// Construct the two-seat network battle from both decks + the master seed (design F-N-5).
/// / are the per-side deck orders the node
/// already computed (BattleSessionState.GetShuffledDeck) and handed each client.
/// / are each seat's class ordinal (1..8,
/// the CardClass 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.
/// NOTE: GameMgr (the leader chara ids set below) is a PROCESS GLOBAL. Setting per-session
/// chara ids is therefore only safe while exactly one engine-backed battle exists at a time — the
/// invariant enforces on the caller side.
public void Setup(int masterSeed,
IReadOnlyList seatADeck, IReadOnlyList seatBDeck,
int seatAClass = 1, int seatBClass = 2)
{
// Prime the engine's process-global statics (CardMaster, Wizard.Data, all-8-class Master,
// GameMgr/netUser/udid). Idempotent (process-once); makes the LIVE host ready so Setup succeeds
// here rather than throwing into the shadow's no-op path (Phase 2 N2, carried-risk A).
EngineGlobalInit.EnsureInitialized();
// rng defaults to SeededRandomSource(masterSeed) inside the mgr — the stream is born aligned
// with the seed the node handed both clients (F-N-5; O-N-2 "bit-aligned anyway").
var mgr = new HeadlessNetworkBattleMgr(new SessionContentsCreator(masterSeed));
// 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+),
// so this collapses them to no-ops without changing authoritative state. Set AFTER construction
// so the ctor still wired the LIVE NetworkBattleReceiver (ND4) rather than the replay receiver.
// Safe for shadow: the only thing !IsRecovery additionally enables is EMIT, which a pure shadow
// never does (it never originates a send).
mgr.IsRecovery = true;
// Seat each player as the other's opponent (private field on BattlePlayerBase, as the real
// match-load does). Mirrors HeadlessFixture.NewNetworkEmitBattle.
BattlePlayerBase player = mgr.GetBattlePlayer(isPlayer: true);
BattlePlayerBase enemy = mgr.GetBattlePlayer(isPlayer: false);
SetField(player, "_opponentBattlePlayer", enemy);
SetField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
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
InstallHeadlessNetworkAgent(); // turn-flow resolve reads ToolboxGame.RealTimeNetworkAgent
// Per-session leader class: chara_id == class_id for 1..8 in the all-8-class ClassCharacterList,
// so writing the seats' class ordinals into GameMgr's DataMgr resolves each leader's correct
// class. Process-global — safe only under EngineSessionGate (see method remarks above).
SetGameMgrCharaIds(seatAClass, seatBClass);
SeedDeck(mgr, seatADeck, isPlayer: true);
SeedDeck(mgr, seatBDeck, isPlayer: false);
_mgr = mgr;
// Use the mgr's OWN receiver — the ctor already wired it to the mgr's OperateReceive +
// NetworkBattleData (NetworkBattleManagerBase.cs:266, non-recovery branch). This is the same
// receiver the engine's RecoveryDataHandler drives when replaying recorded frames.
_receiver = mgr.GetNetworkBattleReceiver();
}
/// Ingest one client frame into the engine for the given seat.
/// maps the sender to the engine's player(true)/opponent(false) seat (F-N-2). A throw/reject is
/// returned as a detected-desync EVENT (ND6), never silently absorbed.
public EngineIngestResult Receive(MsgEnvelope env, bool isPlayerSeat)
{
if (_mgr is null || _receiver is null)
throw new InvalidOperationException("Receive before Setup.");
var dict = ToEngineDict((env.Body as RawBody)?.Entries);
var uri = MapUri(env.Uri);
try
{
// Mirror the engine's own recorded-frame replay (RecoveryDataHandler.cs:283): every
// ingested action resolves through the isHaveSequence ConductReceiveData path, and
// checkBreakData:false so a partial/handshake frame is not rejected as a break.
bool accepted = _receiver.ReceivedMessage(
uri, isHaveSequence: true, dict, isPlayerSeat, handler: null, checkBreakData: false);
return accepted ? EngineIngestResult.Ok() : EngineIngestResult.Reject($"receiver rejected {env.Uri}");
}
catch (Exception ex)
{
var site = ex.StackTrace?.Split('\n').FirstOrDefault()?.Trim();
return EngineIngestResult.Reject($"{env.Uri} threw: {ex.GetType().Name}: {ex.Message} @ {site}");
}
}
// --- live board-state reads (N1 oracle surface; design F-N-4 board-state reads) ----------------
// Each returns LIVE engine state off the seated player, mirroring the Phase-1 oracle reads
// (VanillaFollowerOracleTests: player.Pp, player.HandCardList.Count, ClassAndInPlayCardList,
// leader == the Class card). seat:true == player, false == opponent (F-N-2).
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;
/// Followers in play, excluding the leader (the Class card occupies one slot of
/// ClassAndInPlayCardList).
public int BoardCount(bool playerSeat) => Math.Max(0, Seat(playerSeat).ClassAndInPlayCardList.Count - 1);
private engine::BattlePlayerBase Seat(bool playerSeat) =>
(_mgr ?? throw new InvalidOperationException("read before Setup")).GetBattlePlayer(playerSeat);
private static NetworkBattleDefine.NetworkBattleURI MapUri(NetworkBattleUri uri)
=> Enum.Parse(uri.ToString());
// The receiver reads keys via Enum.IsDefined over NetworkParameter and casts nested values to
// List