diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index d5fab1e..ff8b33a 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -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 ExtractIdxList(MsgEnvelope env) - { - if (env.Body is not RawBody rawBody) return Array.Empty(); - if (rawBody.Entries.TryGetValue("idxList", out var raw) && raw is System.Collections.IEnumerable seq && raw is not string) - { - var result = new List(); - 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(); - } } diff --git a/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs b/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs new file mode 100644 index 0000000..c911a83 --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs @@ -0,0 +1,76 @@ +using SVSim.BattleNode.Lifecycle; +using SVSim.BattleNode.Protocol; +using SVSim.BattleNode.Protocol.Bodies; + +namespace SVSim.BattleNode.Sessions.Dispatch; + +/// Server-synthesized control/broadcast frames + inbound-body helpers, relocated verbatim +/// from BattleSession so the per-URI handlers can build them. Pure: no session state. +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 ExtractIdxList(MsgEnvelope env) + { + if (env.Body is not RawBody rawBody) return Array.Empty(); + if (rawBody.Entries.TryGetValue("idxList", out var raw) && raw is System.Collections.IEnumerable seq && raw is not string) + { + var result = new List(); + 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(); + } +}