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.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(); diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index c38179bd..4cde5f49 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; @@ -155,6 +155,9 @@ public void OnPeerEvent(NetcodePlayer player, 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/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index cd55d1f8..52b24b9a 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); @@ -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; } @@ -371,13 +371,12 @@ void UpdateStats(ref ProtocolState.PackagesStats stats) void OnConsistencyCheck(object? sender, ElapsedEventArgs e) { if (state.CurrentStatus is not ProtocolStatus.Running) return; - var lastReceivedFrame = inbox.LastReceivedInput.Frame; var checkFrame = lastReceivedFrame.Number - options.ConsistencyCheckDistance; 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; @@ -385,9 +384,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 +394,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/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..85d30e2e 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 { @@ -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; } @@ -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; } } @@ -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)); } @@ -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); } @@ -296,7 +295,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) { @@ -308,11 +307,21 @@ 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; } - 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; @@ -323,7 +332,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/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 55465a04..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 NetcodePlayer Player = player; - public SynchronizingEventInfo Synchronizing = default; - public SynchronizedEventInfo Synchronized = default; - public ConnectionInterruptedEventInfo NetworkInterrupted = default; + public readonly NetcodePlayer Player = player; + public PeerEventInfo EventInfo = eventInfo; + public readonly PeerEvent Type => EventInfo.Type; public readonly bool TryFormat( Span utf8Destination, out int bytesWritten, ReadOnlySpan format, @@ -29,23 +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; - switch (Type) - { - 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); - } + 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/Options/ProtocolOptions.cs b/src/Backdash/Options/ProtocolOptions.cs index 4757fa79..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; @@ -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 8 /// @@ -180,8 +180,15 @@ 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 180 + /// + /// + public int ConsistencyCheckStoreSize { get; set; } = 250; /// /// Enable/Disable consistency check. 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 b8b35fcd..1b97a1be 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -99,12 +99,26 @@ SessionServices services 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, + services.ChecksumStore, localConnections, inputComparer ) @@ -112,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, - services.StateStore - ); - ConfigureJobs(services); } @@ -730,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: @@ -776,10 +754,14 @@ void OnNetworkEvent(in ProtocolEventInfo evt) break; } - callbacks.OnPeerEvent(player, new(PeerEvent.Disconnected)); break; - default: - logger.Write(LogLevel.Warning, $"Unknown protocol event {evt} from {player}"); + + 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 234722ed..43d810cb 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(); + networkEventQueue = new(); PeerConnectionFactory peerConnectionFactory = new( networkEventQueue, services.Random, logger, udp, - options.Protocol, options.TimeSync, stateStore + options.Protocol, options.TimeSync, services.ChecksumStore ); ProtocolState protocolState = @@ -254,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 or PeerEvent.ChecksumMismatch: 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; } } diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index 222f7d99..c3de6921 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, + services.ChecksumStore, new(options.NumberOfPlayers), services.InputComparer ) 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}"); } diff --git a/src/Backdash/Session/SessionServices.cs b/src/Backdash/Session/SessionServices.cs index 36eb521d..8b807703 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; } @@ -46,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); 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(); } 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..cf5ed902 --- /dev/null +++ b/src/Backdash/Synchronizing/State/ChecksumStore.cs @@ -0,0 +1,31 @@ +namespace Backdash.Synchronizing.State; + +sealed class ChecksumStore +{ + readonly Entry[] data; + + public ChecksumStore(int size) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size); + data = new Entry[size]; + } + + 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; + } + + struct Entry + { + public Frame Frame; + public uint Checksum; + } +} 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); }