From 82b7d1e9406602828d6e18459ac4079331882325 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 31 May 2026 22:05:16 -0400 Subject: [PATCH] feat(battle-node): OutboundSequencer assigns playSeq + archives for Resume --- .../Reliability/OutboundSequencer.cs | 27 ++++++++++ .../Reliability/OutboundSequencerTests.cs | 52 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 SVSim.BattleNode/Reliability/OutboundSequencer.cs create mode 100644 SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs diff --git a/SVSim.BattleNode/Reliability/OutboundSequencer.cs b/SVSim.BattleNode/Reliability/OutboundSequencer.cs new file mode 100644 index 0000000..da7d7a3 --- /dev/null +++ b/SVSim.BattleNode/Reliability/OutboundSequencer.cs @@ -0,0 +1,27 @@ +using SVSim.BattleNode.Protocol; + +namespace SVSim.BattleNode.Reliability; + +/// +/// Per-session outbound ledger. Assigns monotonic playSeq to ordered pushes and archives +/// them for future Resume retransmit (v2). No-stock control pushes (BattleFinish/JudgeResult/Resume) +/// are wrapped with no playSeq and skip the archive. +/// +public sealed class OutboundSequencer +{ + private long _next = 1; + private readonly Dictionary _archive = new(); + + public IReadOnlyDictionary Archive => _archive; + + public MsgEnvelope AssignAndArchive(MsgEnvelope envelope) + { + var seq = _next++; + var stamped = envelope with { PlaySeq = seq }; + _archive[seq] = stamped; + return stamped; + } + + public MsgEnvelope WrapNoStock(MsgEnvelope envelope) => + envelope with { PlaySeq = null }; +} diff --git a/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs b/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs new file mode 100644 index 0000000..21e0698 --- /dev/null +++ b/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using SVSim.BattleNode.Protocol; +using SVSim.BattleNode.Reliability; + +namespace SVSim.UnitTests.BattleNode.Reliability; + +[TestFixture] +public class OutboundSequencerTests +{ + private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri) => + new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0, Cat: EmitCategory.Battle, + PubSeq: null, PlaySeq: null, Body: new Dictionary()); + + [Test] + public void AssignAndArchive_FirstCall_ReturnsEnvelopeWithPlaySeq1() + { + var seq = new OutboundSequencer(); + var assigned = seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.BattleStart)); + + Assert.That(assigned.PlaySeq, Is.EqualTo(1)); + } + + [Test] + public void AssignAndArchive_SubsequentCalls_ReturnContiguousSequence() + { + var seq = new OutboundSequencer(); + Assert.That(seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Matched)).PlaySeq, Is.EqualTo(1)); + Assert.That(seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.BattleStart)).PlaySeq, Is.EqualTo(2)); + Assert.That(seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Deal)).PlaySeq, Is.EqualTo(3)); + } + + [Test] + public void NoStockControlPush_DoesNotAssignPlaySeqOrArchive() + { + var seq = new OutboundSequencer(); + var env = seq.WrapNoStock(MakeEnvelope(NetworkBattleUri.BattleFinish)); + + Assert.That(env.PlaySeq, Is.Null); + Assert.That(seq.Archive, Is.Empty); + } + + [Test] + public void Archive_ContainsArchivedEnvelopesKeyedByPlaySeq() + { + var seq = new OutboundSequencer(); + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Matched)); + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.BattleStart)); + + Assert.That(seq.Archive.Keys, Is.EquivalentTo(new[] { 1L, 2L })); + Assert.That(seq.Archive[1L].Uri, Is.EqualTo(NetworkBattleUri.Matched)); + } +}