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.
|
// 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
|
// 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.
|
// bot doesn't have, so the bot's TurnEnd now lands here.
|
||||||
case NetworkBattleUri.TurnStart when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
// The `IsRealForwardableFromScripted` guard ensures this arm matches ONLY the
|
||||||
case NetworkBattleUri.TurnEnd when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
// scripted bot's emissions (sender ViewerId == FakeOpponentViewerId) — without
|
||||||
case NetworkBattleUri.Judge when ReferenceEquals(from, B) || ReferenceEquals(from, A):
|
// 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,
|
// Generic forwarder for scripted-bot emissions. The Scripted bot's TurnStart,
|
||||||
// TurnEnd, and Judge are intended for the real participant.
|
// 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));
|
result.Add((other, env, false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -177,6 +191,13 @@ public sealed class BattleSession
|
|||||||
return from.ViewerId == ScriptedLifecycle.FakeOpponentViewerId;
|
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(
|
private MsgEnvelope BuildAck(NetworkBattleUri uri) => new(
|
||||||
uri,
|
uri,
|
||||||
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
||||||
|
|||||||
@@ -260,6 +260,86 @@ public class BattleSessionDispatchTests
|
|||||||
"Swap from A doesn't advance B's phase.");
|
"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()
|
private static (BattleSession, FakeRealParticipant, FakeRealParticipant) NewPvpSession()
|
||||||
{
|
{
|
||||||
var a = new FakeRealParticipant(viewerId: 1001, PlayerACtx());
|
var a = new FakeRealParticipant(viewerId: 1001, PlayerACtx());
|
||||||
@@ -268,6 +348,15 @@ public class BattleSessionDispatchTests
|
|||||||
return (s, a, b);
|
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(
|
private static MatchContext PlayerACtx() => new(
|
||||||
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
|
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
|
||||||
ClassId: "3", CharaId: "3", CardMasterName: "card_master_node_10015",
|
ClassId: "3", CharaId: "3", CardMasterName: "card_master_node_10015",
|
||||||
|
|||||||
Reference in New Issue
Block a user