refactor(battle-node): extract frame factories into BattleFrames

This commit is contained in:
gamer147
2026-06-03 13:52:36 -04:00
parent 85c43a9a72
commit 4f89463f9c
2 changed files with 87 additions and 76 deletions

View File

@@ -67,7 +67,7 @@ public sealed class BattleSession
try
{
await survivor.PushAsync(
BuildBattleFinish(BattleResult.DisconnectWin), noStock: true, cancellation)
BattleFrames.BuildBattleFinish(BattleResult.DisconnectWin), noStock: true, cancellation)
.ConfigureAwait(false);
}
catch (Exception ex)
@@ -143,7 +143,7 @@ public sealed class BattleSession
switch (env.Uri)
{
case NetworkBattleUri.InitNetwork when phaseFrom?.Phase == BattleSessionPhase.AwaitingInitNetwork:
result.Add(new DispatchRoute(from, BuildAck(NetworkBattleUri.InitNetwork), true));
result.Add(new DispatchRoute(from, BattleFrames.BuildAck(NetworkBattleUri.InitNetwork), true));
phaseFrom!.Phase = BattleSessionPhase.AwaitingInitBattle;
break;
@@ -175,7 +175,7 @@ public sealed class BattleSession
case NetworkBattleUri.InitBattle
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
// Ack only — NO Matched push.
result.Add(new DispatchRoute(from, BuildAck(NetworkBattleUri.InitBattle), true));
result.Add(new DispatchRoute(from, BattleFrames.BuildAck(NetworkBattleUri.InitBattle), true));
phaseFrom!.Phase = BattleSessionPhase.AwaitingLoaded;
break;
@@ -194,7 +194,7 @@ public sealed class BattleSession
// Judge to sender ONLY (not broadcast — there's no real other side).
// The client's JudgeOperation → ControlTurnStartPlayer flips back to
// the local AI's turn after this Judge arrives.
result.Add(new DispatchRoute(from, BuildJudgeBroadcast(), false));
result.Add(new DispatchRoute(from, BattleFrames.BuildJudgeBroadcast(), false));
break;
case NetworkBattleUri.InitBattle when phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
@@ -226,7 +226,7 @@ public sealed class BattleSession
case NetworkBattleUri.Swap when phaseFrom?.Phase == BattleSessionPhase.AwaitingSwap:
{
var hand = ScriptedLifecycle.ComputeHandAfterSwap(ExtractIdxList(env));
var hand = ScriptedLifecycle.ComputeHandAfterSwap(BattleFrames.ExtractIdxList(env));
// SwapResponse is always immediate — it completes the sender's own mulligan UI.
result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildSwapResponse(hand), false));
_state.PostSwapHands[from] = hand;
@@ -257,8 +257,8 @@ public sealed class BattleSession
case NetworkBattleUri.TurnEnd when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
if (Type == BattleType.Pvp && BothAfterReady())
{
var turnEndBroadcast = BuildTurnEndBroadcast();
var judgeBroadcast = BuildJudgeBroadcast();
var turnEndBroadcast = BattleFrames.BuildTurnEndBroadcast();
var judgeBroadcast = BattleFrames.BuildJudgeBroadcast();
result.Add(new DispatchRoute(from, turnEndBroadcast, false));
result.Add(new DispatchRoute(other, turnEndBroadcast, false));
result.Add(new DispatchRoute(from, judgeBroadcast, false));
@@ -282,8 +282,8 @@ public sealed class BattleSession
// so the RunAsync cascade doesn't synthesize a follow-up BattleFinish.
case NetworkBattleUri.TurnEndFinal when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
result.Add(new DispatchRoute(other, env, false));
result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.LifeWin), true));
result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.LifeLose), true));
result.Add(new DispatchRoute(from, BattleFrames.BuildBattleFinish(BattleResult.LifeWin), true));
result.Add(new DispatchRoute(other, BattleFrames.BuildBattleFinish(BattleResult.LifeLose), true));
_state.SessionPhase = BattleSessionPhase.Terminal;
break;
@@ -292,8 +292,8 @@ public sealed class BattleSession
// proper retire codes. Bots swallow their push (no real-opponent state).
case NetworkBattleUri.Retire:
case NetworkBattleUri.Kill:
result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.RetireLose), true));
result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.RetireWin), true));
result.Add(new DispatchRoute(from, BattleFrames.BuildBattleFinish(BattleResult.RetireLose), true));
result.Add(new DispatchRoute(other, BattleFrames.BuildBattleFinish(BattleResult.RetireWin), true));
_state.SessionPhase = BattleSessionPhase.Terminal;
break;
@@ -355,69 +355,4 @@ public sealed class BattleSession
(A as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady &&
(B as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady;
private MsgEnvelope BuildAck(NetworkBattleUri uri) => new(
uri,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.General,
PubSeq: null,
PlaySeq: null,
Body: new ResultCodeOnlyBody());
private MsgEnvelope BuildTurnEndBroadcast() => new(
NetworkBattleUri.TurnEnd,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new TurnEndBody(TurnState: 0));
private MsgEnvelope BuildJudgeBroadcast() => new(
NetworkBattleUri.Judge,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new JudgeBody(Spin: ScriptedProfiles.OpponentJudgeSpin));
private MsgEnvelope BuildBattleFinish(BattleResult result) => new(
NetworkBattleUri.BattleFinish,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new BattleFinishBody(Result: result));
private static IReadOnlyList<long> ExtractIdxList(MsgEnvelope env)
{
if (env.Body is not RawBody rawBody) return Array.Empty<long>();
if (rawBody.Entries.TryGetValue("idxList", out var raw) && raw is System.Collections.IEnumerable seq && raw is not string)
{
var result = new List<long>();
foreach (var item in seq)
{
switch (item)
{
case long l: result.Add(l); break;
case int i: result.Add(i); break;
case double d: result.Add((long)d); break;
case decimal m: result.Add((long)m); break;
case string s when long.TryParse(s, out var p): result.Add(p); break;
}
}
return result;
}
return Array.Empty<long>();
}
}

View File

@@ -0,0 +1,76 @@
using SVSim.BattleNode.Lifecycle;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Protocol.Bodies;
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>Server-synthesized control/broadcast frames + inbound-body helpers, relocated verbatim
/// from BattleSession so the per-URI handlers can build them. Pure: no session state.</summary>
internal static class BattleFrames
{
internal static MsgEnvelope BuildAck(NetworkBattleUri uri) => new(
uri,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.General,
PubSeq: null,
PlaySeq: null,
Body: new ResultCodeOnlyBody());
internal static MsgEnvelope BuildTurnEndBroadcast() => new(
NetworkBattleUri.TurnEnd,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new TurnEndBody(TurnState: 0));
internal static MsgEnvelope BuildJudgeBroadcast() => new(
NetworkBattleUri.Judge,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new JudgeBody(Spin: ScriptedProfiles.OpponentJudgeSpin));
internal static MsgEnvelope BuildBattleFinish(BattleResult result) => new(
NetworkBattleUri.BattleFinish,
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
Uuid: WireConstants.ServerUuid,
Bid: null,
Try: 0,
Cat: EmitCategory.Battle,
PubSeq: null,
PlaySeq: null,
Body: new BattleFinishBody(Result: result));
internal static IReadOnlyList<long> ExtractIdxList(MsgEnvelope env)
{
if (env.Body is not RawBody rawBody) return Array.Empty<long>();
if (rawBody.Entries.TryGetValue("idxList", out var raw) && raw is System.Collections.IEnumerable seq && raw is not string)
{
var result = new List<long>();
foreach (var item in seq)
{
switch (item)
{
case long l: result.Add(l); break;
case int i: result.Add(i); break;
case double d: result.Add((long)d); break;
case decimal m: result.Add((long)m); break;
case string s when long.TryParse(s, out var p): result.Add(p); break;
}
}
return result;
}
return Array.Empty<long>();
}
}