feat(battle-node): PvP gameplay-frame forwarding arms
TurnStart / PlayActions / Echo / TurnEndActions / JudgeResult from a real participant in Pvp mode forward to the other participant once BothAfterReady. Scripted's bot-burst case arms (gated on FakeOpponentViewerId) precede the PvP forwarder so they're unaffected. The bot-emission TurnStart/TurnEnd/Judge guard was tightened from `ReferenceEquals(from, A or B)` (always true) to call IsRealForwardableFromScripted directly in the `when` clause. The prior shape used `goto default` to drop non-bot senders, which would have short-circuited the new PvP forwarder for TurnStart in PvP mode.
This commit is contained in:
@@ -149,12 +149,26 @@ public sealed class BattleSession
|
||||
// TurnEnd arm above (gated on session-level Phase) also caught the bot's TurnEnd.
|
||||
// Post-migration that arm gates on the sender's per-participant Phase, which the
|
||||
// bot doesn't have, so the bot's TurnEnd now lands here.
|
||||
case NetworkBattleUri.TurnStart when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
||||
case NetworkBattleUri.TurnEnd when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
||||
case NetworkBattleUri.Judge when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
||||
// The `IsRealForwardableFromScripted` guard ensures this arm matches ONLY the
|
||||
// scripted bot's emissions (sender ViewerId == FakeOpponentViewerId) — without
|
||||
// it, a TurnStart/TurnEnd/Judge from a real participant in PvP mode would match
|
||||
// here and `goto default` would skip the PvP forwarder arm below.
|
||||
case NetworkBattleUri.TurnStart when IsRealForwardableFromScripted(from, env):
|
||||
case NetworkBattleUri.TurnEnd when IsRealForwardableFromScripted(from, env):
|
||||
case NetworkBattleUri.Judge when IsRealForwardableFromScripted(from, env):
|
||||
// Generic forwarder for scripted-bot emissions. The Scripted bot's TurnStart,
|
||||
// TurnEnd, and Judge are intended for the real participant.
|
||||
if (!IsRealForwardableFromScripted(from, env)) goto default;
|
||||
result.Add((other, env, false));
|
||||
break;
|
||||
|
||||
// --- PvP gameplay forwarding (post-AfterReady).
|
||||
// Order matters: this MUST come after the FakeOpponentViewerId arms so
|
||||
// Scripted bot emissions don't fall into the PvP forwarder.
|
||||
case NetworkBattleUri.TurnStart when Type == BattleType.Pvp && BothAfterReady():
|
||||
case NetworkBattleUri.PlayActions when Type == BattleType.Pvp && BothAfterReady():
|
||||
case NetworkBattleUri.Echo when Type == BattleType.Pvp && BothAfterReady():
|
||||
case NetworkBattleUri.TurnEndActions when Type == BattleType.Pvp && BothAfterReady():
|
||||
case NetworkBattleUri.JudgeResult when Type == BattleType.Pvp && BothAfterReady():
|
||||
result.Add((other, env, false));
|
||||
break;
|
||||
|
||||
@@ -177,6 +191,13 @@ public sealed class BattleSession
|
||||
return from.ViewerId == ScriptedLifecycle.FakeOpponentViewerId;
|
||||
}
|
||||
|
||||
// Phase 2: PvP gameplay-frame forwarding is gated on BOTH sides having completed
|
||||
// the handshake (i.e. reached AfterReady). Until then, an early TurnStart/PlayActions
|
||||
// from one side has no valid recipient.
|
||||
private bool BothAfterReady() =>
|
||||
(A as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady &&
|
||||
(B as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady;
|
||||
|
||||
private MsgEnvelope BuildAck(NetworkBattleUri uri) => new(
|
||||
uri,
|
||||
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
||||
|
||||
@@ -260,6 +260,86 @@ public class BattleSessionDispatchTests
|
||||
"Swap from A doesn't advance B's phase.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_TurnStart_from_A_in_BothAfterReady_forwards_to_B()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
DriveToAfterReady(s, b);
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.TurnStart));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_PlayActions_from_A_in_BothAfterReady_forwards_to_B()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
DriveToAfterReady(s, b);
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.PlayActions));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.PlayActions));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_Echo_from_A_in_BothAfterReady_forwards_to_B()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
DriveToAfterReady(s, b);
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Echo));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_TurnEndActions_from_A_in_BothAfterReady_forwards_to_B()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
DriveToAfterReady(s, b);
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.TurnEndActions));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_JudgeResult_from_A_in_BothAfterReady_forwards_to_B()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
DriveToAfterReady(s, b);
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.JudgeResult));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Pvp_PlayActions_when_B_still_AwaitingSwap_drops()
|
||||
{
|
||||
var (s, a, b) = NewPvpSession();
|
||||
DriveToAfterReady(s, a);
|
||||
// B is still AwaitingInitNetwork — BothAfterReady is false.
|
||||
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.PlayActions));
|
||||
|
||||
Assert.That(routes, Is.Empty,
|
||||
"PvP gameplay forwarding must wait until BOTH sides reach AfterReady.");
|
||||
}
|
||||
|
||||
private static (BattleSession, FakeRealParticipant, FakeRealParticipant) NewPvpSession()
|
||||
{
|
||||
var a = new FakeRealParticipant(viewerId: 1001, PlayerACtx());
|
||||
@@ -268,6 +348,15 @@ public class BattleSessionDispatchTests
|
||||
return (s, a, b);
|
||||
}
|
||||
|
||||
private static void DriveToAfterReady(BattleSession s, FakeRealParticipant p)
|
||||
{
|
||||
s.ComputeFrames(p, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(p, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeFrames(p, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeFrames(p, NewEnvelope(NetworkBattleUri.Swap));
|
||||
// p.Phase should now be AfterReady.
|
||||
}
|
||||
|
||||
private static MatchContext PlayerACtx() => new(
|
||||
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
|
||||
ClassId: "3", CharaId: "3", CardMasterName: "card_master_node_10015",
|
||||
|
||||
Reference in New Issue
Block a user