feat(battlenode): Receive ingests a captured PlayActions headless (Phase 2 N0)
Receive feeds the decoded frame into the mgr's own NetworkBattleReceiver (isHaveSequence:true, checkBreakData:false — mirroring the engine's RecoveryDataHandler frame replay), reboxing object?->object for nested data. No engine gaps surfaced; the only fix was a test-harness one (load all deck ids in a single HeadlessCardMaster.Load — per-id calls each replace the master). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ extern alias engine;
|
||||
using System.Reflection;
|
||||
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;
|
||||
@@ -28,6 +30,7 @@ internal sealed class SessionBattleEngine
|
||||
private const int DefaultLeaderLife = 20;
|
||||
|
||||
private HeadlessNetworkBattleMgr? _mgr;
|
||||
private NetworkBattleReceiver? _receiver;
|
||||
|
||||
/// <summary>True once Setup has built the two-seat battle.</summary>
|
||||
public bool IsReady => _mgr is not null;
|
||||
@@ -58,12 +61,61 @@ internal sealed class SessionBattleEngine
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Ingest one client frame into the engine for the given seat. <paramref name="isPlayerSeat"/>
|
||||
/// maps the sender to the engine's player(true)/opponent(false) seat (F-N-2).</summary>
|
||||
/// 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.</summary>
|
||||
public EngineIngestResult Receive(MsgEnvelope env, bool isPlayerSeat)
|
||||
=> throw new NotImplementedException("Filled by Task 4 (ingest probe).");
|
||||
{
|
||||
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)
|
||||
{
|
||||
return EngineIngestResult.Reject($"{env.Uri} threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static NetworkBattleDefine.NetworkBattleURI MapUri(NetworkBattleUri uri)
|
||||
=> Enum.Parse<NetworkBattleDefine.NetworkBattleURI>(uri.ToString());
|
||||
|
||||
// The receiver reads keys via Enum.IsDefined over NetworkParameter and casts nested values to
|
||||
// List<object> / Dictionary<string,object>; the node decodes nested data as the nullable
|
||||
// List<object?> / Dictionary<string,object?>. Rebox to the non-nullable shape, dropping nulls
|
||||
// (the receiver presence-checks keys, so an absent key is the correct encoding of a null).
|
||||
private static Dictionary<string, object> ToEngineDict(Dictionary<string, object?>? entries)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
if (entries is null) return result;
|
||||
foreach (var (k, v) in entries)
|
||||
if (v is not null) result[k] = Rebox(v);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static object Rebox(object v) => v switch
|
||||
{
|
||||
Dictionary<string, object?> d => d.Where(kv => kv.Value is not null)
|
||||
.ToDictionary(kv => kv.Key, kv => Rebox(kv.Value!)),
|
||||
List<object?> l => l.Where(x => x is not null).Select(x => Rebox(x!)).ToList(),
|
||||
_ => v,
|
||||
};
|
||||
|
||||
// --- headless wiring (production analogue of HeadlessFixture) -----------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user