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:
gamer147
2026-06-01 21:46:28 -04:00
parent 8a97dd0194
commit 72dc1887d9
2 changed files with 114 additions and 4 deletions

View File

@@ -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",