From 83af3a40e0d199e2fb003cd580f394b9e5dbfad6 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 15:14:58 -0300 Subject: [PATCH 01/12] WIP --- src/Backdash/Options/ProtocolOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Backdash/Options/ProtocolOptions.cs b/src/Backdash/Options/ProtocolOptions.cs index 4757fa79..a11961cf 100644 --- a/src/Backdash/Options/ProtocolOptions.cs +++ b/src/Backdash/Options/ProtocolOptions.cs @@ -167,10 +167,10 @@ internal bool IsNetworkPackageStatsEnabled() => /// Offset to be applied to frame on checksum consistency check. /// The frame sent is (LastReceivedFrame - ConsistencyCheckOffset). /// - /// Defaults to 8 + /// Defaults to 1 /// /// - public int ConsistencyCheckDistance { get; set; } = 8; + public int ConsistencyCheckDistance { get; set; } = 1; /// /// The time to wait before send next consistency check (0 to disable). From 3f37c690dbcd726ac0fe12073e688d485080f265 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 15:50:19 -0300 Subject: [PATCH 02/12] WIP --- src/Backdash/Options/ProtocolOptions.cs | 8 +++++++ .../Session/Backends/RemoteSession.cs | 3 +++ .../Session/Backends/SyncTestSession.cs | 1 + .../Synchronizing/Input/Synchronizer.cs | 7 +++++- .../Synchronizing/State/ChecksumStore.cs | 23 +++++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/Backdash/Synchronizing/State/ChecksumStore.cs diff --git a/src/Backdash/Options/ProtocolOptions.cs b/src/Backdash/Options/ProtocolOptions.cs index a11961cf..42b2f272 100644 --- a/src/Backdash/Options/ProtocolOptions.cs +++ b/src/Backdash/Options/ProtocolOptions.cs @@ -183,6 +183,14 @@ internal bool IsNetworkPackageStatsEnabled() => public TimeSpan ConsistencyCheckInterval { get; set; } = TimeSpan.FromMilliseconds(3_000); + /// + /// The number of checksum frames that will be keep for consistency checks. + /// + /// Defaults to 1 + /// + /// + public int ConsistencyCheckStoreSize { get; set; } = 180; + /// /// Enable/Disable consistency check. /// diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index b8b35fcd..5af46d5b 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -22,6 +22,7 @@ sealed class RemoteSession : INetcodeSession where TInput : unma readonly Logger logger; readonly PeerClient udp; readonly Synchronizer synchronizer; + readonly ChecksumStore checksumStore; readonly NetcodeJobManager jobManager; readonly PeerConnectionFactory peerConnectionFactory; readonly IDeterministicRandom random; @@ -94,6 +95,7 @@ SessionServices services endpoints = new(Max.NumberOfPlayers); spectators = []; peerObservers = new(); + checksumStore = new(options); callbacks = services.SessionHandler; if (this.options.SaveConfirmedInputHistory) @@ -105,6 +107,7 @@ SessionServices services addedPlayers, services.StateStore, services.ChecksumProvider, + checksumStore, localConnections, inputComparer ) diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index 222f7d99..abed429c 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -94,6 +94,7 @@ SessionServices services addedPlayers.Keys, services.StateStore, services.ChecksumProvider, + new(options), new(options.NumberOfPlayers), services.InputComparer ) diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 2deb9c23..2610b8da 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -18,6 +18,7 @@ sealed class Synchronizer where TInput : unmanaged readonly IReadOnlyCollection players; readonly IStateStore stateStore; readonly IChecksumProvider checksumProvider; + readonly ChecksumStore checksumStore; readonly ConnectionsState localConnections; readonly EqualityComparer inputComparer; readonly List> inputQueues; @@ -35,6 +36,7 @@ public Synchronizer( IReadOnlyCollection players, IStateStore stateStore, IChecksumProvider checksumProvider, + ChecksumStore checksumStore, ConnectionsState localConnections, EqualityComparer? inputComparer = null ) @@ -46,10 +48,12 @@ public Synchronizer( this.checksumProvider = checksumProvider; this.localConnections = localConnections; this.inputComparer = inputComparer ?? EqualityComparer.Default; + this.checksumStore = checksumStore; inputQueues = new(2); endianness = options.GetEndiannessNumberStateSerializer(); - stateStore.Initialize(options.TotalSavedFramesAllowed); + var saveBufferSize = options.TotalSavedFramesAllowed; + stateStore.Initialize(saveBufferSize); } public bool InRollback { get; private set; } @@ -242,6 +246,7 @@ public void SaveCurrentFrame() Callbacks.SaveState(currentFrame, ref writer); nextState.Frame = currentFrame; nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); + checksumStore.Add(nextState.Frame, nextState.Checksum); stateStore.Advance(); logger.Write(LogLevel.Trace, $"sync: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); diff --git a/src/Backdash/Synchronizing/State/ChecksumStore.cs b/src/Backdash/Synchronizing/State/ChecksumStore.cs new file mode 100644 index 00000000..2d935bee --- /dev/null +++ b/src/Backdash/Synchronizing/State/ChecksumStore.cs @@ -0,0 +1,23 @@ +using Backdash.Data; +using Backdash.Options; + +namespace Backdash.Synchronizing.State; + +sealed class ChecksumStore +{ + readonly CircularBuffer data; + + public ChecksumStore(int size) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size); + data = new(size); + } + + public ChecksumStore(NetcodeOptions options) : this(Math.Max(options.TotalSavedFramesAllowed, + options.Protocol.ConsistencyCheckStoreSize)) { } + + public void Add(Frame frame, uint checksum) { } + + + public uint Get(Frame frame) => 0; +} From 34c7b05525191bb57879c8203a06d57ef6f8d70d Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 15:54:53 -0300 Subject: [PATCH 03/12] WIP --- src/Backdash/Network/PeerConnection.cs | 8 ++++---- src/Backdash/Network/PeerConnectionFactory.cs | 6 +++--- .../Network/Protocol/Comm/ProtocolInbox.cs | 4 ++-- src/Backdash/Session/Backends/RemoteSession.cs | 2 +- .../Session/Backends/SpectatorSession.cs | 3 ++- .../Synchronizing/State/DefaultStateStore.cs | 18 ------------------ .../Synchronizing/State/IStateStore.cs | 6 ------ 7 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index cd55d1f8..837329b5 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -25,7 +25,7 @@ sealed class PeerConnection : IDisposable where TInput : unmanaged readonly ProtocolInbox inbox; readonly ProtocolOutbox outbox; readonly ProtocolInputBuffer inputBuffer; - readonly IStateStore stateStore; + readonly ChecksumStore checksumStore; readonly Timer qualityReportTimer; readonly Timer networkStatsTimer; @@ -45,7 +45,7 @@ public PeerConnection( ProtocolInbox inbox, ProtocolOutbox outbox, ProtocolInputBuffer inputBuffer, - IStateStore stateStore + ChecksumStore checksumStore ) { this.options = options; @@ -57,7 +57,7 @@ IStateStore stateStore this.inbox = inbox; this.outbox = outbox; this.inputBuffer = inputBuffer; - this.stateStore = stateStore; + this.checksumStore = checksumStore; disconnectCheckEnabled = options.IsDisconnectTimeoutEnabled(); keepAliveTimer = new(options.KeepAliveInterval); @@ -377,7 +377,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) if (checkFrame <= 1) return; state.Consistency.AskedFrame = new(checkFrame); - state.Consistency.AskedChecksum = stateStore.GetChecksum(state.Consistency.AskedFrame); + state.Consistency.AskedChecksum = checksumStore.Get(state.Consistency.AskedFrame); if (state.Consistency.AskedFrame.IsNull || state.Consistency.AskedChecksum is 0) return; diff --git a/src/Backdash/Network/PeerConnectionFactory.cs b/src/Backdash/Network/PeerConnectionFactory.cs index 32e3250e..928b6823 100644 --- a/src/Backdash/Network/PeerConnectionFactory.cs +++ b/src/Backdash/Network/PeerConnectionFactory.cs @@ -17,7 +17,7 @@ sealed class PeerConnectionFactory( PeerClient peer, ProtocolOptions options, TimeSyncOptions timeSyncOptions, - IStateStore stateStore + ChecksumStore checksumStore ) { public PeerConnection Create( @@ -31,13 +31,13 @@ public PeerConnection Create( var outbox = new ProtocolOutbox(state, peer, logger); var syncManager = new ProtocolSynchronizer(logger, random, state, options, outbox, networkEventHandler); var inbox = new ProtocolInbox(options, inputSerializer, state, syncManager, outbox, - networkEventHandler, inputEventQueue, stateStore, logger); + networkEventHandler, inputEventQueue, checksumStore, logger); var inputBuffer = new ProtocolInputBuffer(options, inputSerializer, state, logger, timeSync, outbox, inbox); PeerConnection connection = new( options, state, logger, timeSync, networkEventHandler, - syncManager, inbox, outbox, inputBuffer, stateStore + syncManager, inbox, outbox, inputBuffer, checksumStore ); state.StoppingToken.Register(() => connection.Disconnect()); diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 400eecd5..5ce70219 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -27,7 +27,7 @@ sealed class ProtocolInbox( IMessageSender messageSender, IProtocolNetworkEventHandler networkEvents, IProtocolInputEventPublisher inputEvents, - IStateStore stateStore, + ChecksumStore checksumStore, Logger logger ) : IProtocolInbox where TInput : unmanaged { @@ -323,7 +323,7 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) { var checkFrame = message.ConsistencyCheckRequest.Frame; - var checksum = stateStore.GetChecksum(checkFrame); + var checksum = checksumStore.Get(checkFrame); logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum:x8})"); diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 5af46d5b..86e5de24 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -124,7 +124,7 @@ SessionServices services udp, this.options.Protocol, this.options.TimeSync, - services.StateStore + checksumStore ); ConfigureJobs(services); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 234722ed..04b351ae 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -80,10 +80,11 @@ SessionServices services ConfigureJobs(services); var magicNumber = services.Random.SyncNumber(); + var checksumStore = new ChecksumStore(options); networkEventQueue = new(); PeerConnectionFactory peerConnectionFactory = new( networkEventQueue, services.Random, logger, udp, - options.Protocol, options.TimeSync, stateStore + options.Protocol, options.TimeSync, checksumStore ); ProtocolState protocolState = diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index 0c8ee14b..df938346 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -66,22 +66,4 @@ public SavedFrame Last() /// public void Advance() => head = (head + 1) % savedStates.Length; - - /// - public uint GetChecksum(Frame frame) - { - var span = savedStates.AsSpan(); - ref var current = ref MemoryMarshal.GetReference(span); - ref var limit = ref Unsafe.Add(ref current, span.Length); - - while (Unsafe.IsAddressLessThan(ref current, ref limit)) - { - if (current.Frame.Number == frame.Number) - return current.Checksum; - - current = ref Unsafe.Add(ref current, 1)!; - } - - return 0; - } } diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index 2ddf6766..c47d237b 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -33,10 +33,4 @@ public interface IStateStore /// Advance the store pointer /// void Advance(); - - /// - /// Finds checksum for - /// - /// - uint GetChecksum(Frame frame); } From d475df8ffd8c75e0f0e7164433db68c92073291f Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 16:14:59 -0300 Subject: [PATCH 04/12] WIP --- .../Session/Backends/RemoteSession.cs | 29 +++++++++---------- .../Session/Backends/SpectatorSession.cs | 4 +-- .../Session/Backends/SyncTestSession.cs | 2 +- src/Backdash/Session/SessionServices.cs | 2 ++ .../Synchronizing/State/ChecksumStore.cs | 28 +++++++++++------- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 86e5de24..6f0d9cfb 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -22,7 +22,6 @@ sealed class RemoteSession : INetcodeSession where TInput : unma readonly Logger logger; readonly PeerClient udp; readonly Synchronizer synchronizer; - readonly ChecksumStore checksumStore; readonly NetcodeJobManager jobManager; readonly PeerConnectionFactory peerConnectionFactory; readonly IDeterministicRandom random; @@ -95,19 +94,31 @@ SessionServices services endpoints = new(Max.NumberOfPlayers); spectators = []; peerObservers = new(); - checksumStore = new(options); callbacks = services.SessionHandler; if (this.options.SaveConfirmedInputHistory) inputListener = new MemoryInputListener(inputListener); + + udp = services.ProtocolClientFactory.CreateClient(options.LocalPort, peerObservers); + + peerConnectionFactory = new( + networkEventQueue, + services.Random, + logger, + udp, + this.options.Protocol, + this.options.TimeSync, + services.ChecksumStore + ); + synchronizer = new( this.options, logger, addedPlayers, services.StateStore, services.ChecksumProvider, - checksumStore, + services.ChecksumStore, localConnections, inputComparer ) @@ -115,18 +126,6 @@ SessionServices services Callbacks = callbacks, }; - udp = services.ProtocolClientFactory.CreateClient(options.LocalPort, peerObservers); - - peerConnectionFactory = new( - networkEventQueue, - services.Random, - logger, - udp, - this.options.Protocol, - this.options.TimeSync, - checksumStore - ); - ConfigureJobs(services); } diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 04b351ae..e619856f 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -80,11 +80,11 @@ SessionServices services ConfigureJobs(services); var magicNumber = services.Random.SyncNumber(); - var checksumStore = new ChecksumStore(options); + networkEventQueue = new(); PeerConnectionFactory peerConnectionFactory = new( networkEventQueue, services.Random, logger, udp, - options.Protocol, options.TimeSync, checksumStore + options.Protocol, options.TimeSync, services.ChecksumStore ); ProtocolState protocolState = diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index abed429c..c3de6921 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -94,7 +94,7 @@ SessionServices services addedPlayers.Keys, services.StateStore, services.ChecksumProvider, - new(options), + services.ChecksumStore, new(options.NumberOfPlayers), services.InputComparer ) diff --git a/src/Backdash/Session/SessionServices.cs b/src/Backdash/Session/SessionServices.cs index 36eb521d..571fc964 100644 --- a/src/Backdash/Session/SessionServices.cs +++ b/src/Backdash/Session/SessionServices.cs @@ -18,6 +18,7 @@ sealed class SessionServices where TInput : unmanaged public NetcodeJobManager JobManager { get; } public ProtocolClientFactory ProtocolClientFactory { get; } public IStateStore StateStore { get; } + public ChecksumStore ChecksumStore { get; } public IRandomNumberGenerator Random { get; } public IDeterministicRandom DeterministicRandom { get; } public ILatencyStrategy LatencyStrategy { get; } @@ -39,6 +40,7 @@ public SessionServices( ArgumentNullException.ThrowIfNull(inputSerializer); ArgumentNullException.ThrowIfNull(options); + ChecksumStore = new(Math.Max(options.TotalSavedFramesAllowed, options.Protocol.ConsistencyCheckStoreSize)); ChecksumProvider = services?.ChecksumProvider ?? new Fletcher32ChecksumProvider(); StateStore = services?.StateStore ?? new DefaultStateStore(options.StateSizeHint); InputListener = services?.InputListener; diff --git a/src/Backdash/Synchronizing/State/ChecksumStore.cs b/src/Backdash/Synchronizing/State/ChecksumStore.cs index 2d935bee..cf5ed902 100644 --- a/src/Backdash/Synchronizing/State/ChecksumStore.cs +++ b/src/Backdash/Synchronizing/State/ChecksumStore.cs @@ -1,23 +1,31 @@ -using Backdash.Data; -using Backdash.Options; - namespace Backdash.Synchronizing.State; sealed class ChecksumStore { - readonly CircularBuffer data; + readonly Entry[] data; public ChecksumStore(int size) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size); - data = new(size); + data = new Entry[size]; } - public ChecksumStore(NetcodeOptions options) : this(Math.Max(options.TotalSavedFramesAllowed, - options.Protocol.ConsistencyCheckStoreSize)) { } - - public void Add(Frame frame, uint checksum) { } + public void Add(Frame frame, uint checksum) + { + ref var entry = ref data[frame.Number % data.Length]; + entry.Frame = frame; + entry.Checksum = checksum; + } + public uint Get(Frame frame) + { + var entry = data[frame.Number % data.Length]; + return entry.Frame == frame ? entry.Checksum : 0; + } - public uint Get(Frame frame) => 0; + struct Entry + { + public Frame Frame; + public uint Checksum; + } } From ca9a8da288abdb72b6c68bf8945e3e4930efc652 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 17:05:35 -0300 Subject: [PATCH 05/12] wip --- src/Backdash/Network/PeerConnection.cs | 7 ++----- src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs | 8 ++++---- src/Backdash/Options/ProtocolOptions.cs | 7 +++---- src/Backdash/Synchronizing/Input/InputQueue.cs | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index 837329b5..f24df628 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -372,7 +372,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) { if (state.CurrentStatus is not ProtocolStatus.Running) return; - var lastReceivedFrame = inbox.LastReceivedInput.Frame; + var lastReceivedFrame = inbox.LastAckedFrame; var checkFrame = lastReceivedFrame.Number - options.ConsistencyCheckDistance; if (checkFrame <= 1) return; @@ -385,9 +385,6 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) if (state.Consistency.LastCheck is 0) state.Consistency.LastCheck = Stopwatch.GetTimestamp(); - logger.Write(LogLevel.Trace, - $"Start consistency check for frame {state.Consistency.AskedFrame} #{state.Consistency.AskedChecksum:x8}"); - var elapsed = Stopwatch.GetElapsedTime(state.Consistency.LastCheck); if (options.ConsistencyCheckTimeout > TimeSpan.Zero && elapsed > options.ConsistencyCheckTimeout) { @@ -398,7 +395,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) } logger.Write(LogLevel.Debug, - $"Send consistency request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum:x8}"); + $"Begin consistency-check request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum:x8}"); outbox .SendMessage(new(MessageType.ConsistencyCheckRequest) diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 5ce70219..7f370fbb 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -54,7 +54,7 @@ public void OnPeerMessage(ref readonly ProtocolMessage message, in SocketAddress { if (state.CurrentStatus is not ProtocolStatus.Running) { - logger.Write(LogLevel.Debug, $"recv skip (not ready): {message} on {state.Player}"); + logger.Write(LogLevel.Trace, $"recv skip (not ready): {message} on {state.Player}"); return; } @@ -186,7 +186,7 @@ bool OnInput(in InputMessage msg) lastReceivedInput.Frame = currentFrame; state.Stats.LastReceivedInputTime = Stopwatch.GetTimestamp(); currentFrame++; - logger.Write(LogLevel.Debug, + logger.Write(LogLevel.Trace, $"Received input: frame {lastReceivedInput.Frame}, sending to emulator queue {state.Player} (ack: {LastAckedFrame})"); inputEvents.Publish(new(state.Player, lastReceivedInput)); } @@ -296,7 +296,7 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) var checksum = message.ConsistencyCheckReply.Checksum; var localChecksum = state.Consistency.AskedChecksum; - logger.Write(LogLevel.Debug, $"Received consistency request reply for: {checkFrame} #{checksum:x8}"); + logger.Write(LogLevel.Debug, $"Reply consistency-check for {checkFrame} #{checksum:x8}"); if (state.Consistency.AskedFrame != checkFrame || localChecksum is 0 || checksum is 0) { @@ -312,7 +312,7 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) return false; } - logger.Write(LogLevel.Debug, $"Consistency request check for: {checkFrame} OK({checksum:x8})"); + logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum:x8}"); state.Consistency.LastCheck = Stopwatch.GetTimestamp(); state.Consistency.AskedFrame = Frame.Null; state.Consistency.AskedChecksum = 0; diff --git a/src/Backdash/Options/ProtocolOptions.cs b/src/Backdash/Options/ProtocolOptions.cs index 42b2f272..cb6372ac 100644 --- a/src/Backdash/Options/ProtocolOptions.cs +++ b/src/Backdash/Options/ProtocolOptions.cs @@ -165,7 +165,7 @@ internal bool IsNetworkPackageStatsEnabled() => /// /// Offset to be applied to frame on checksum consistency check. - /// The frame sent is (LastReceivedFrame - ConsistencyCheckOffset). + /// The frame sent is (LastAckedFrame - ConsistencyCheckOffset). /// /// Defaults to 1 /// @@ -180,13 +180,12 @@ internal bool IsNetworkPackageStatsEnabled() => /// Defaults to 3_000 milliseconds /// /// - public TimeSpan ConsistencyCheckInterval { get; set; } = - TimeSpan.FromMilliseconds(3_000); + public TimeSpan ConsistencyCheckInterval { get; set; } = TimeSpan.FromSeconds(3); /// /// The number of checksum frames that will be keep for consistency checks. /// - /// Defaults to 1 + /// Defaults to 180 /// /// public int ConsistencyCheckStoreSize { get; set; } = 180; diff --git a/src/Backdash/Synchronizing/Input/InputQueue.cs b/src/Backdash/Synchronizing/Input/InputQueue.cs index 7d447c63..b8159201 100644 --- a/src/Backdash/Synchronizing/Input/InputQueue.cs +++ b/src/Backdash/Synchronizing/Input/InputQueue.cs @@ -213,7 +213,7 @@ void AddDelayedInputToQueue(GameInput input, in Frame inputFrame) // we can dump out of prediction mode entirely. Otherwise, advance the prediction frame count up. if (prediction.Frame.Number == lastFrameRequested.Number && firstIncorrectFrame.IsNull) { - logger.Write(LogLevel.Debug, + logger.Write(LogLevel.Trace, $"Queue {QueueId} => prediction is correct! dumping out of prediction mode."); prediction.ResetFrame(); } From 1e0ba7dbd9e25dcb57374c7786be8ba69d57b765 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 17:28:29 -0300 Subject: [PATCH 06/12] wip --- src/Backdash/Network/PeerConnection.cs | 3 +-- src/Backdash/Options/ProtocolOptions.cs | 8 ++++---- src/Backdash/Session/SessionServices.cs | 5 +++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index f24df628..a8f7bebf 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -371,8 +371,7 @@ void UpdateStats(ref ProtocolState.PackagesStats stats) void OnConsistencyCheck(object? sender, ElapsedEventArgs e) { if (state.CurrentStatus is not ProtocolStatus.Running) return; - - var lastReceivedFrame = inbox.LastAckedFrame; + var lastReceivedFrame = inbox.LastReceivedInput.Frame; var checkFrame = lastReceivedFrame.Number - options.ConsistencyCheckDistance; if (checkFrame <= 1) return; diff --git a/src/Backdash/Options/ProtocolOptions.cs b/src/Backdash/Options/ProtocolOptions.cs index cb6372ac..2894eb50 100644 --- a/src/Backdash/Options/ProtocolOptions.cs +++ b/src/Backdash/Options/ProtocolOptions.cs @@ -143,7 +143,7 @@ internal bool IsDisconnectTimeoutEnabled() => /// /// /// Defaults to - public bool NetworkPackageStatsEnabled { get; set; } = false; + public bool NetworkPackageStatsEnabled { get; set; } internal bool IsNetworkPackageStatsEnabled() => NetworkPackageStatsEnabled && NetworkPackageStatsInterval > TimeSpan.Zero; @@ -167,10 +167,10 @@ internal bool IsNetworkPackageStatsEnabled() => /// Offset to be applied to frame on checksum consistency check. /// The frame sent is (LastAckedFrame - ConsistencyCheckOffset). /// - /// Defaults to 1 + /// Defaults to 8 /// /// - public int ConsistencyCheckDistance { get; set; } = 1; + public int ConsistencyCheckDistance { get; set; } = 8; /// /// The time to wait before send next consistency check (0 to disable). @@ -188,7 +188,7 @@ internal bool IsNetworkPackageStatsEnabled() => /// Defaults to 180 /// /// - public int ConsistencyCheckStoreSize { get; set; } = 180; + public int ConsistencyCheckStoreSize { get; set; } = 250; /// /// Enable/Disable consistency check. diff --git a/src/Backdash/Session/SessionServices.cs b/src/Backdash/Session/SessionServices.cs index 571fc964..8b807703 100644 --- a/src/Backdash/Session/SessionServices.cs +++ b/src/Backdash/Session/SessionServices.cs @@ -40,7 +40,6 @@ public SessionServices( ArgumentNullException.ThrowIfNull(inputSerializer); ArgumentNullException.ThrowIfNull(options); - ChecksumStore = new(Math.Max(options.TotalSavedFramesAllowed, options.Protocol.ConsistencyCheckStoreSize)); ChecksumProvider = services?.ChecksumProvider ?? new Fletcher32ChecksumProvider(); StateStore = services?.StateStore ?? new DefaultStateStore(options.StateSizeHint); InputListener = services?.InputListener; @@ -48,11 +47,13 @@ public SessionServices( LatencyStrategy = DelayStrategyFactory.Create(Random, options.Protocol.LatencyStrategy); InputComparer = services?.InputComparer ?? EqualityComparer.Default; InputSerializer = inputSerializer; - DeterministicRandom = services?.DeterministicRandom ?? new XorShiftRandom(); if (DeterministicRandom.InitialSeed is 0) DeterministicRandom.SetInitialSeed(options.DeterministicRandomInitialSeed); + ChecksumStore = new(Math.Max(options.TotalSavedFramesAllowed, options.Protocol.ConsistencyCheckStoreSize) + + options.Protocol.ConsistencyCheckDistance); + var logWriter = services?.LogWriter ?? new ConsoleTextLogWriter(); Logger = new(options.Logger, logWriter); JobManager = new(Logger); From c93c07f0d3e2911d88040fa44a37a7aca2899429 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 17:30:48 -0300 Subject: [PATCH 07/12] wip --- src/Backdash/Network/Protocol/ProtocolEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Backdash/Network/Protocol/ProtocolEvent.cs b/src/Backdash/Network/Protocol/ProtocolEvent.cs index 55465a04..089dfa34 100644 --- a/src/Backdash/Network/Protocol/ProtocolEvent.cs +++ b/src/Backdash/Network/Protocol/ProtocolEvent.cs @@ -16,7 +16,7 @@ enum ProtocolEvent : byte struct ProtocolEventInfo(ProtocolEvent type, NetcodePlayer player) : IUtf8SpanFormattable { public readonly ProtocolEvent Type = type; - public NetcodePlayer Player = player; + public readonly NetcodePlayer Player = player; public SynchronizingEventInfo Synchronizing = default; public SynchronizedEventInfo Synchronized = default; public ConnectionInterruptedEventInfo NetworkInterrupted = default; From 0b4ae94a55b260f8fc4f962b7f81c52c9355f3c2 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 17:41:30 -0300 Subject: [PATCH 08/12] WIP --- samples/ConsoleGame/Game.cs | 6 +++--- samples/SpaceWar.Shared/GameSession.cs | 2 +- src/Backdash/Session/INetcodeSessionHandler.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/ConsoleGame/Game.cs b/samples/ConsoleGame/Game.cs index 86b284b9..1762688c 100644 --- a/samples/ConsoleGame/Game.cs +++ b/samples/ConsoleGame/Game.cs @@ -150,11 +150,11 @@ public void TimeSync(FrameSpan framesAhead) Thread.Sleep(framesAhead.Duration()); } - public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt) + public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) { Log($"PEER EVENT: {evt} from {player}"); - if (player.IsSpectator()) - return; + if (player.IsSpectator()) return; + switch (evt.Type) { case PeerEvent.Connected: diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index c38179bd..c32ac17d 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -122,7 +122,7 @@ void UpdateStats() nonGameState.StateSize = saved.Size; } - public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt) + public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) { Log($"=> PEER EVENT: {evt} from {player}"); if (player.IsSpectator()) return; diff --git a/src/Backdash/Session/INetcodeSessionHandler.cs b/src/Backdash/Session/INetcodeSessionHandler.cs index 758e8f50..6b9cae2f 100644 --- a/src/Backdash/Session/INetcodeSessionHandler.cs +++ b/src/Backdash/Session/INetcodeSessionHandler.cs @@ -57,7 +57,7 @@ public interface INetcodeSessionHandler /// /// The player owner of the event /// Event data - void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt); + void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt); /// /// Reads a state object of a snapshot. @@ -91,7 +91,7 @@ public void TimeSync(FrameSpan framesAhead) => logger.Write(LogLevel.Information, $"{DateTime.UtcNow:o} [Session Handler] Need to sync: {framesAhead} frames ahead"); - public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt) => + public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) => logger.Write(LogLevel.Information, $"{DateTime.UtcNow:o} [Session Handler] {nameof(OnPeerEvent)} called with {evt} for {player}"); } From d36cef9c8a519fe53bcdb55933e99ee7231bf4c0 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 17:44:43 -0300 Subject: [PATCH 09/12] wip --- .../Network/Protocol/ProtocolEvent.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Backdash/Network/Protocol/ProtocolEvent.cs b/src/Backdash/Network/Protocol/ProtocolEvent.cs index 089dfa34..18e5f25d 100644 --- a/src/Backdash/Network/Protocol/ProtocolEvent.cs +++ b/src/Backdash/Network/Protocol/ProtocolEvent.cs @@ -32,20 +32,14 @@ public readonly bool TryFormat( if (!writer.Write(" ProtoEvt "u8)) return false; if (!writer.WriteEnum(Type)) return false; if (!writer.Write(":"u8)) return false; - switch (Type) + return Type switch { - case ProtocolEvent.NetworkInterrupted: - return writer.Write("Timeout: "u8) - && writer.Write(NetworkInterrupted.DisconnectTimeout); - case ProtocolEvent.Synchronizing when !writer.Write(' '): - return false; - case ProtocolEvent.Synchronizing: - return writer.Write(Synchronizing.CurrentStep) - && writer.Write('/') - && - writer.Write(Synchronizing.TotalSteps); - default: - return writer.Write("{}"u8); - } + ProtocolEvent.NetworkInterrupted => writer.Write("Timeout: "u8) && + writer.Write(NetworkInterrupted.DisconnectTimeout), + ProtocolEvent.Synchronizing when !writer.Write(' ') => false, + ProtocolEvent.Synchronizing => writer.Write(Synchronizing.CurrentStep) && writer.Write('/') && + writer.Write(Synchronizing.TotalSteps), + _ => writer.Write("{}"u8), + }; } } From e6718f76464a0d27169444fa052de4e816e52c19 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 18:23:39 -0300 Subject: [PATCH 10/12] fix sample --- samples/SpaceWar.Lobby/Game1.cs | 5 +++-- samples/SpaceWar.Lobby/Program.cs | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/SpaceWar.Lobby/Game1.cs b/samples/SpaceWar.Lobby/Game1.cs index d29cdc48..9cb74929 100644 --- a/samples/SpaceWar.Lobby/Game1.cs +++ b/samples/SpaceWar.Lobby/Game1.cs @@ -33,13 +33,14 @@ protected override void Initialize() protected override void LoadContent() { SpriteBatch = new(GraphicsDevice); - SceneManager = new(this, startScene: new ChooseLobbyScene()); Services.AddService(SpriteBatch); - Services.AddService(SceneManager); Services.AddService(settings); Services.AddService(new GameAssets(Content, GraphicsDevice)); Services.AddService(new LobbyHttpClient(settings)); + + SceneManager = new(this, startScene: new ChooseLobbyScene()); + Services.AddService(SceneManager); } protected override void Update(GameTime gameTime) diff --git a/samples/SpaceWar.Lobby/Program.cs b/samples/SpaceWar.Lobby/Program.cs index 28f07919..ca180a9b 100644 --- a/samples/SpaceWar.Lobby/Program.cs +++ b/samples/SpaceWar.Lobby/Program.cs @@ -2,6 +2,5 @@ var settings = AppSettings.LoadFromJson("appsettings.json"); settings.ParseArgs(args); - -using var game = new Game1(settings); +using Game1 game = new(settings); game.Run(); From cdf5585c3b6aa89b5e22e63f8ce6814a30057fd9 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 18:25:28 -0300 Subject: [PATCH 11/12] cleanup protocol event type --- src/Backdash/Network/PeerConnection.cs | 6 +-- .../Network/Protocol/Comm/ProtocolInbox.cs | 17 ++++----- .../Protocol/Comm/ProtocolSynchronizer.cs | 2 +- .../Network/Protocol/ProtocolEvent.cs | 34 +++-------------- .../Network/ProtocolNetworkEventQueue.cs | 3 +- .../Session/Backends/RemoteSession.cs | 38 +++---------------- .../Session/Backends/SpectatorSession.cs | 36 ++---------------- 7 files changed, 29 insertions(+), 107 deletions(-) diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index a8f7bebf..52b24b9a 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -269,9 +269,9 @@ bool DispatchInterruptedEvent(TimeSpan timeout) if (state.Connection is not { DisconnectNotifySent: false, DisconnectEventSent: false }) return false; - networkEventHandler.OnNetworkEvent(new(ProtocolEvent.NetworkInterrupted, state.Player) + networkEventHandler.OnNetworkEvent(state.Player, new(PeerEvent.ConnectionInterrupted) { - NetworkInterrupted = new() + ConnectionInterrupted = new() { DisconnectTimeout = timeout, }, @@ -291,7 +291,7 @@ bool DispatchDisconnectEvent() return false; state.Connection.DisconnectEventSent = true; - networkEventHandler.OnNetworkEvent(ProtocolEvent.Disconnected, state.Player); + networkEventHandler.OnNetworkEvent(PeerEvent.Disconnected, state.Player); return true; } diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 7f370fbb..f377fe69 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -85,7 +85,7 @@ public void OnPeerMessage(ref readonly ProtocolMessage message, in SocketAddress state.Stats.Received.TotalBytes += (ByteSize)bytesReceived; if (state.Connection.DisconnectNotifySent && state.CurrentStatus is ProtocolStatus.Running) { - networkEvents.OnNetworkEvent(ProtocolEvent.NetworkResumed, state.Player); + networkEvents.OnNetworkEvent(PeerEvent.ConnectionResumed, state.Player); state.Connection.DisconnectNotifySent = false; } } @@ -124,7 +124,7 @@ bool OnInput(in InputMessage msg) if (state.CurrentStatus is not ProtocolStatus.Disconnected && !state.Connection.DisconnectEventSent) { logger.Write(LogLevel.Information, "Disconnecting endpoint on remote request"); - networkEvents.OnNetworkEvent(ProtocolEvent.Disconnected, state.Player); + networkEvents.OnNetworkEvent(PeerEvent.Disconnected, state.Player); state.Connection.DisconnectEventSent = true; } } @@ -236,7 +236,7 @@ bool OnSyncReply(ref readonly ProtocolMessage msg, ref ProtocolMessage replyMsg) if (!state.Connection.IsConnected) { - networkEvents.OnNetworkEvent(ProtocolEvent.Connected, state.Player); + networkEvents.OnNetworkEvent(PeerEvent.Connected, state.Player); state.Connection.IsConnected = true; } @@ -254,21 +254,20 @@ bool OnSyncReply(ref readonly ProtocolMessage msg, ref ProtocolMessage replyMsg) state.Stats.RoundTripTime = ping; lastReceivedInput.ResetFrame(); state.RemoteSyncNumber = msg.Header.SyncNumber; - networkEvents.OnNetworkEvent(new(ProtocolEvent.Synchronized, state.Player) + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.Synchronized) { Synchronized = new(ping), }); } else { - networkEvents.OnNetworkEvent( - new(ProtocolEvent.Synchronizing, state.Player) - { - Synchronizing = new( + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.Synchronizing) + { + Synchronizing = new( TotalSteps: options.NumberOfSyncRoundTrips, CurrentStep: options.NumberOfSyncRoundTrips - state.Sync.RemainingRoundTrips ), - } + } ); sync.CreateRequestMessage(ref replyMsg); } diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolSynchronizer.cs b/src/Backdash/Network/Protocol/Comm/ProtocolSynchronizer.cs index 33061c61..83575e4f 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolSynchronizer.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolSynchronizer.cs @@ -68,7 +68,7 @@ public void Update() active = false; logger.Write(LogLevel.Warning, $"Fail to sync {state.Player} after {retryCounter} retries"); - eventHandler.OnNetworkEvent(ProtocolEvent.SyncFailure, state.Player); + eventHandler.OnNetworkEvent(PeerEvent.SynchronizationFailure, state.Player); return; } diff --git a/src/Backdash/Network/Protocol/ProtocolEvent.cs b/src/Backdash/Network/Protocol/ProtocolEvent.cs index 18e5f25d..74c47a4d 100644 --- a/src/Backdash/Network/Protocol/ProtocolEvent.cs +++ b/src/Backdash/Network/Protocol/ProtocolEvent.cs @@ -2,24 +2,11 @@ namespace Backdash.Network.Protocol; -enum ProtocolEvent : byte +struct ProtocolEventInfo(NetcodePlayer player, PeerEventInfo eventInfo) : IUtf8SpanFormattable { - Connected, - Synchronizing, - Synchronized, - SyncFailure, - Disconnected, - NetworkInterrupted, - NetworkResumed, -} - -struct ProtocolEventInfo(ProtocolEvent type, NetcodePlayer player) : IUtf8SpanFormattable -{ - public readonly ProtocolEvent Type = type; public readonly NetcodePlayer Player = player; - public SynchronizingEventInfo Synchronizing = default; - public SynchronizedEventInfo Synchronized = default; - public ConnectionInterruptedEventInfo NetworkInterrupted = default; + public PeerEventInfo EventInfo = eventInfo; + public readonly PeerEvent Type => EventInfo.Type; public readonly bool TryFormat( Span utf8Destination, out int bytesWritten, ReadOnlySpan format, @@ -29,17 +16,8 @@ public readonly bool TryFormat( Utf8StringBuilder writer = new(in utf8Destination, ref bytesWritten); if (!writer.Write("P"u8)) return false; if (!writer.Write(Player.Index)) return false; - if (!writer.Write(" ProtoEvt "u8)) return false; - if (!writer.WriteEnum(Type)) return false; - if (!writer.Write(":"u8)) return false; - return Type switch - { - ProtocolEvent.NetworkInterrupted => writer.Write("Timeout: "u8) && - writer.Write(NetworkInterrupted.DisconnectTimeout), - ProtocolEvent.Synchronizing when !writer.Write(' ') => false, - ProtocolEvent.Synchronizing => writer.Write(Synchronizing.CurrentStep) && writer.Write('/') && - writer.Write(Synchronizing.TotalSteps), - _ => writer.Write("{}"u8), - }; + if (!writer.Write(" Info: "u8)) return false; + if (!writer.Write(EventInfo)) return false; + return true; } } diff --git a/src/Backdash/Network/ProtocolNetworkEventQueue.cs b/src/Backdash/Network/ProtocolNetworkEventQueue.cs index 04447b75..7b05709a 100644 --- a/src/Backdash/Network/ProtocolNetworkEventQueue.cs +++ b/src/Backdash/Network/ProtocolNetworkEventQueue.cs @@ -6,7 +6,8 @@ namespace Backdash.Network; interface IProtocolNetworkEventHandler : IDisposable { void OnNetworkEvent(in ProtocolEventInfo evt); - void OnNetworkEvent(in ProtocolEvent evt, NetcodePlayer player) => OnNetworkEvent(new(evt, player)); + void OnNetworkEvent(NetcodePlayer player, PeerEventInfo evt) => OnNetworkEvent(new(player, evt)); + void OnNetworkEvent(PeerEvent evt, NetcodePlayer player) => OnNetworkEvent(player, new(evt)); } sealed class ProtocolNetworkEventQueue : IProtocolNetworkEventHandler diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 6f0d9cfb..6f437514 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -732,42 +732,18 @@ void RemoveSpectator(NetcodePlayer player) void OnNetworkEvent(in ProtocolEventInfo evt) { - ref readonly var player = ref evt.Player; + var player = evt.Player; logger.Write(LogLevel.Trace, $"Session event: {evt} from {player}"); + callbacks.OnPeerEvent(player, in evt.EventInfo); switch (evt.Type) { - case ProtocolEvent.Connected: - callbacks.OnPeerEvent(player, new(PeerEvent.Connected)); - break; - case ProtocolEvent.Synchronizing: - callbacks.OnPeerEvent(player, new(PeerEvent.Synchronizing) - { - Synchronizing = new(evt.Synchronizing.CurrentStep, evt.Synchronizing.TotalSteps), - }); - break; - case ProtocolEvent.Synchronized: - callbacks.OnPeerEvent(player, new(PeerEvent.Synchronized) - { - Synchronized = new(evt.Synchronized.Ping), - }); - break; - case ProtocolEvent.SyncFailure: + case PeerEvent.SynchronizationFailure: if (player.IsSpectator()) RemoveSpectator(player); - else - callbacks.OnPeerEvent(player, new(PeerEvent.SynchronizationFailure)); break; - case ProtocolEvent.NetworkInterrupted: - callbacks.OnPeerEvent(player, new(PeerEvent.ConnectionInterrupted) - { - ConnectionInterrupted = new(evt.NetworkInterrupted.DisconnectTimeout), - }); - break; - case ProtocolEvent.NetworkResumed: - callbacks.OnPeerEvent(player, new(PeerEvent.ConnectionResumed)); - break; - case ProtocolEvent.Disconnected: + + case PeerEvent.Disconnected: switch (player.Type) { case PlayerType.Spectator: @@ -778,10 +754,6 @@ void OnNetworkEvent(in ProtocolEventInfo evt) break; } - callbacks.OnPeerEvent(player, new(PeerEvent.Disconnected)); - break; - default: - logger.Write(LogLevel.Warning, $"Unknown protocol event {evt} from {player}"); break; } } diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index e619856f..f2745456 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -255,46 +255,18 @@ void ConsumeProtocolNetworkEvents() void OnNetworkEvent(in ProtocolEventInfo evt) { - ref readonly var player = ref evt.Player; + callbacks.OnPeerEvent(evt.Player, in evt.EventInfo); + switch (evt.Type) { - case ProtocolEvent.Connected: - callbacks.OnPeerEvent(player, new(PeerEvent.Connected)); - break; - case ProtocolEvent.Synchronizing: - callbacks.OnPeerEvent(player, new(PeerEvent.Synchronizing) - { - Synchronizing = new(evt.Synchronizing.CurrentStep, evt.Synchronizing.TotalSteps), - }); - break; - case ProtocolEvent.Synchronized: - callbacks.OnPeerEvent(player, new(PeerEvent.Synchronized) - { - Synchronized = new(evt.Synchronized.Ping), - }); + case PeerEvent.Synchronized: callbacks.OnSessionStart(); isSynchronizing = false; host.Start(); break; - case ProtocolEvent.SyncFailure: - callbacks.OnPeerEvent(player, new(PeerEvent.SynchronizationFailure)); + case PeerEvent.SynchronizationFailure: Close(); break; - case ProtocolEvent.NetworkInterrupted: - callbacks.OnPeerEvent(player, new(PeerEvent.ConnectionInterrupted) - { - ConnectionInterrupted = new(evt.NetworkInterrupted.DisconnectTimeout), - }); - break; - case ProtocolEvent.NetworkResumed: - callbacks.OnPeerEvent(player, new(PeerEvent.ConnectionResumed)); - break; - case ProtocolEvent.Disconnected: - callbacks.OnPeerEvent(player, new(PeerEvent.Disconnected)); - break; - default: - logger.Write(LogLevel.Warning, $"Unknown protocol event {evt} from {player}"); - break; } } From 9ebf34cdee4515bb4b6c3d7dc64134049d746f20 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 14 Apr 2026 18:52:17 -0300 Subject: [PATCH 12/12] WIP --- samples/SpaceWar.Shared/GameSession.cs | 3 +++ .../Network/Protocol/Comm/ProtocolInbox.cs | 12 ++++++++- src/Backdash/Player/PeerEvent.cs | 26 +++++++++++++++++-- .../Session/Backends/RemoteSession.cs | 8 ++++++ .../Session/Backends/SpectatorSession.cs | 2 +- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index c32ac17d..4cde5f49 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -155,6 +155,9 @@ public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) case PeerEvent.Disconnected: nonGameState.SetConnectState(player, PlayerConnectState.Disconnected); break; + case PeerEvent.ChecksumMismatch: + Log($"=> CHECKSUM MISMATCH: {evt.ChecksumMismatch}"); + break; } } diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index f377fe69..85d30e2e 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -307,7 +307,17 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) { logger.Write(LogLevel.Error, $"Invalid remote checksum on frame {checkFrame}, {localChecksum:x8} != {checksum:x8}"); - state.StoppingTokenSource.Cancel(); + + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) + { + ChecksumMismatch = new( + MismatchFrame: checkFrame, + LocalChecksum: localChecksum, + RemoteChecksum: checksum + ), + } + ); + return false; } diff --git a/src/Backdash/Player/PeerEvent.cs b/src/Backdash/Player/PeerEvent.cs index b0f5ce6a..5625491f 100644 --- a/src/Backdash/Player/PeerEvent.cs +++ b/src/Backdash/Player/PeerEvent.cs @@ -50,6 +50,12 @@ public enum PeerEvent : sbyte /// . /// ConnectionResumed, + + /// + /// When consistency-check has failure. + /// . + /// + ChecksumMismatch, } /// @@ -86,6 +92,12 @@ public readonly struct PeerEventInfo(PeerEvent type) : IUtf8SpanFormattable [field: FieldOffset(HeaderSize)] public ConnectionInterruptedEventInfo ConnectionInterrupted { get; init; } + /// + /// Data for event. + /// + [field: FieldOffset(HeaderSize)] + public ChecksumMismatchEventInfo ChecksumMismatch { get; init; } + /// public override string ToString() { @@ -94,6 +106,7 @@ public override string ToString() PeerEvent.Synchronizing => Synchronizing.ToString(), PeerEvent.Synchronized => Synchronized.ToString(), PeerEvent.ConnectionInterrupted => ConnectionInterrupted.ToString(), + PeerEvent.ChecksumMismatch => ChecksumMismatch.ToString(), _ => "{}", }; return $"Event {Type}: {details}"; @@ -121,12 +134,16 @@ public bool TryFormat( if (!writer.Write(Synchronizing.TotalSteps)) return false; return true; case PeerEvent.Synchronized: - if (!writer.Write(" with ping "u8)) return false; + if (!writer.Write(" ping: "u8)) return false; return writer.Write(Synchronized.Ping.TotalMilliseconds, "f2"); case PeerEvent.ConnectionInterrupted: - if (!writer.Write(" with timeout "u8)) return false; + if (!writer.Write(" timeout: "u8)) return false; if (!writer.Write(ConnectionInterrupted.DisconnectTimeout)) return false; return true; + case PeerEvent.ChecksumMismatch: + if (!writer.Write(" frame: "u8)) return false; + if (!writer.Write(ChecksumMismatch.MismatchFrame)) return false; + return true; default: return writer.Write(' '); } @@ -151,3 +168,8 @@ public bool TryFormat( /// /// Current ping public readonly record struct SynchronizedEventInfo(TimeSpan Ping); + +/// +/// Data for event. +/// +public readonly record struct ChecksumMismatchEventInfo(Frame MismatchFrame, uint LocalChecksum, uint RemoteChecksum); diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 6f437514..1b97a1be 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -755,6 +755,14 @@ void OnNetworkEvent(in ProtocolEventInfo evt) } break; + + case PeerEvent.ChecksumMismatch: + if (player.Type is PlayerType.Spectator) + RemoveSpectator(player); + else + Close(); + + break; } } diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index f2745456..43d810cb 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -264,7 +264,7 @@ void OnNetworkEvent(in ProtocolEventInfo evt) isSynchronizing = false; host.Start(); break; - case PeerEvent.SynchronizationFailure: + case PeerEvent.SynchronizationFailure or PeerEvent.ChecksumMismatch: Close(); break; }