diff --git a/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs b/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs index e1dc4a32..ca555643 100644 --- a/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs +++ b/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs @@ -44,9 +44,9 @@ public void Setup() public sealed class Fletcher32SpanChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) + public Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; var buffer = MemoryMarshal.Cast(data); uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -75,7 +75,7 @@ public uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } @@ -84,9 +84,9 @@ public sealed class Fletcher32UnsafeChecksumProvider : IChecksumProvider const int BlockSize = 360; /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -121,16 +121,16 @@ public unsafe uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } public sealed class Crc32ChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) + public Checksum Compute(ReadOnlySpan data) { - if (data.Length == 0) return 0; + if (data.Length == 0) return Checksum.Empty; uint sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; @@ -146,17 +146,16 @@ public uint Compute(ReadOnlySpan data) } var sum = sum3 + (sum2 << 8) + (sum1 << 16) + (sum0 << 24); - - return sum; + return (Checksum)sum; } } public sealed class Crc32BigEndianChecksumProvider : IChecksumProvider { /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.Length == 0) return 0; + if (data.Length == 0) return Checksum.Empty; fixed (byte* ptr = data) { @@ -203,7 +202,7 @@ public unsafe uint Compute(ReadOnlySpan data) break; } - return sum; + return (Checksum)sum; } } } diff --git a/samples/ConsoleGame/GameState.cs b/samples/ConsoleGame/GameState.cs index 51be8822..e6578f75 100644 --- a/samples/ConsoleGame/GameState.cs +++ b/samples/ConsoleGame/GameState.cs @@ -31,7 +31,7 @@ public class NonGameState public bool IsRunning; public float SyncProgress; public string LastError = ""; - public uint Checksum; + public Checksum Checksum; public PlayerStatus RemotePlayerStatus; public DateTime LostConnectionTime; public TimeSpan DisconnectTimeout; diff --git a/samples/ConsoleGame/Util.cs b/samples/ConsoleGame/Util.cs deleted file mode 100644 index f2abc561..00000000 --- a/samples/ConsoleGame/Util.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Backdash.Serialization; - -namespace ConsoleGame; - -[BinarySerializer] -public partial class GameStateSerializer; diff --git a/samples/ConsoleGame/View.cs b/samples/ConsoleGame/View.cs index 56aff3bc..2e274c2b 100644 --- a/samples/ConsoleGame/View.cs +++ b/samples/ConsoleGame/View.cs @@ -169,7 +169,7 @@ static void DrawStats(GameState currentState, NonGameState nonGameState) $""" Ping: {peer.Ping.TotalMilliseconds:f4} ms Rollback: {info.RollbackFrames} - Checksum: {nonGameState.Checksum:x8} + Checksum: {nonGameState.Checksum} Rng Seed: {currentState.RandomSeed:x8} """ ); diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index 4cde5f49..64f5c868 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -117,7 +117,7 @@ public void TimeSync(FrameSpan framesAhead) void UpdateStats() { nonGameState.RollbackFrames = session.RollbackFrames; - var saved = session.GetCurrentSavedFrame(); + var saved = session.GetSavedState(); nonGameState.StateChecksum = saved.Checksum; nonGameState.StateSize = saved.Size; } @@ -156,7 +156,7 @@ public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) nonGameState.SetConnectState(player, PlayerConnectState.Disconnected); break; case PeerEvent.ChecksumMismatch: - Log($"=> CHECKSUM MISMATCH: {evt.ChecksumMismatch}"); + Log($"=> CHECKSUM MISMATCH: {player.EndPoint} => {evt.ChecksumMismatch}"); break; } } @@ -164,7 +164,10 @@ public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) // used by SyncTest, the return value is used on the state desync handler call object? INetcodeSessionHandler.CreateState(Frame frame, ref readonly BinaryBufferReader reader) { - GameState state = new(); + GameState state = new() + { + Ships = new Ship[session.NumberOfPlayers], + }; state.LoadState(in reader); return state; } diff --git a/samples/SpaceWar.Shared/Logic/Renderer.cs b/samples/SpaceWar.Shared/Logic/Renderer.cs index ab23502b..66a6ce05 100644 --- a/samples/SpaceWar.Shared/Logic/Renderer.cs +++ b/samples/SpaceWar.Shared/Logic/Renderer.cs @@ -279,7 +279,7 @@ void DrawStateStats(GameState gs, NonGameState ngs) const int statsPadding = 2; stateInfoString.Clear(); - stateInfoString.Append($"State: {ngs.StateChecksum:x8} {ngs.StateSize}"); + stateInfoString.Append($"State: {ngs.StateChecksum} {ngs.StateSize}"); var size = gameAssets.MainFont.MeasureString(stateInfoString) * statsScale; Vector2 pos = new((gs.Bounds.Width - size.X) / 2, gs.Bounds.Top - Config.WindowPadding); diff --git a/samples/SpaceWar/Game1.cs b/samples/SpaceWar/Game1.cs index ef341f5f..0b11655e 100644 --- a/samples/SpaceWar/Game1.cs +++ b/samples/SpaceWar/Game1.cs @@ -165,12 +165,24 @@ void HandleNonGameKeys() if (keyboard.IsKeyPressed(Keys.Escape)) Exit(); + if (keyboard.IsKeyPressed(Keys.Z)) + { + foreach (var s in session.EnumerateStateStrings().Take(3)) + { + Console.WriteLine($"--> State(Frame: {s.Frame.Number}, Checksum: {s.Checksum}"); + Console.WriteLine(s.State); + Console.WriteLine($"--> End State(Frame: {s.Frame.Number})"); + } + + return; + } + if (session.IsOnline()) return; if (keyboard.IsKeyPressed(Keys.S)) { - snapshot = session.CurrentStateSnapshot(); + snapshot = session.GetStateSnapshot(); session.WriteLog($"Snapshot saved {snapshot?.Frame}"); } else if (keyboard.IsKeyPressed(Keys.L) && snapshot is not null) diff --git a/src/Backdash/Core/Builders/Utf8StringBuilder.cs b/src/Backdash/Core/Builders/Utf8StringBuilder.cs index 80279880..fa2ea83a 100644 --- a/src/Backdash/Core/Builders/Utf8StringBuilder.cs +++ b/src/Backdash/Core/Builders/Utf8StringBuilder.cs @@ -71,13 +71,13 @@ public bool WriteEnum(in T value, ReadOnlySpan format = default) where } } -readonly ref struct Utf8ObjectStringWriter +readonly ref struct Utf8ObjectStringBuilder { readonly Utf8StringBuilder writer; readonly int firstOffset; readonly ref int offset; - public Utf8ObjectStringWriter(in Span bufferArg, ref int offset) + public Utf8ObjectStringBuilder(in Span bufferArg, ref int offset) { writer = new(in bufferArg, ref offset); this.offset = ref offset; diff --git a/src/Backdash/Core/Checksum.cs b/src/Backdash/Core/Checksum.cs new file mode 100644 index 00000000..c9b21357 --- /dev/null +++ b/src/Backdash/Core/Checksum.cs @@ -0,0 +1,144 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Backdash.Core; + +namespace Backdash; + +/// +/// Value representation of a Checksum +/// +[Serializable] +[DebuggerDisplay("{ToString()}")] +[JsonConverter(typeof(JsonConverter))] +public readonly record struct Checksum : + IComparable, + IComparable, + IEquatable, + IUtf8SpanFormattable, + ISpanFormattable, + IComparisonOperators, + IComparisonOperators +{ + /// Return frame value 0 + public static readonly Checksum Empty = new(0); + + /// Returns the value for the current . + public readonly uint Value; + + /// + /// Initialize new for frame . + /// + /// + public Checksum(uint value) => Value = value; + + /// + public int CompareTo(Checksum other) => Value.CompareTo(other.Value); + + /// + public int CompareTo(uint other) => Value.CompareTo(other); + + /// + public bool Equals(uint other) => Value == other; + + /// Return true if current value is 0 + public bool IsEmpty => Value is 0u; + + const string DefaultFormat = "x8"; + + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string? format, + IFormatProvider? formatProvider + ) => + Value.ToString(format ?? DefaultFormat, formatProvider); + + /// + public override string ToString() => ToString(null, null); + + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string format) => ToString(format, null); + + /// + public bool TryFormat( + Span utf8Destination, out int bytesWritten, + ReadOnlySpan format, + IFormatProvider? provider + ) + { + bytesWritten = 0; + if (format.IsEmpty) format = DefaultFormat; + Utf8StringBuilder writer = new(in utf8Destination, ref bytesWritten); + return writer.Write(Value, format); + } + + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, + IFormatProvider? provider) + { + if (format.IsEmpty) format = DefaultFormat; + return Value.TryFormat(destination, out charsWritten, format, provider); + } + + /// + public static bool operator >(Checksum left, Checksum right) => left.Value > right.Value; + + /// + public static bool operator >=(Checksum left, Checksum right) => left.Value >= right.Value; + + /// + public static bool operator <(Checksum left, Checksum right) => left.Value < right.Value; + + /// + public static bool operator <=(Checksum left, Checksum right) => left.Value <= right.Value; + + /// + public static bool operator ==(Checksum left, uint right) => left.Value == right; + + /// + public static bool operator !=(Checksum left, uint right) => left.Value != right; + + /// + public static bool operator >(Checksum left, uint right) => left.Value > right; + + /// + public static bool operator >=(Checksum left, uint right) => left.Value >= right; + + /// + public static bool operator <(Checksum left, uint right) => left.Value < right; + + /// + public static bool operator <=(Checksum left, uint right) => left.Value <= right; + + /// + public static implicit operator uint(Checksum frame) => frame.Value; + + /// + public static explicit operator Checksum(uint frame) => new(frame); + + internal sealed class JsonConverter : JsonConverter + { + public override Checksum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? Empty : new(uint.Parse(value, NumberStyles.HexNumber, null)); + } + + public override void Write(Utf8JsonWriter writer, Checksum value, JsonSerializerOptions options) + { + Span buffer = stackalloc char[8]; + if (value.TryFormat(buffer, out var charCount, DefaultFormat, null)) + { + writer.WriteStringValue(buffer[..charCount]); + } + else + writer.WriteStringValue(value.ToString(DefaultFormat, null)); + } + } +} diff --git a/src/Backdash/Core/Frame/Frame.cs b/src/Backdash/Core/Frame/Frame.cs index a2d7ff5e..b3829c52 100644 --- a/src/Backdash/Core/Frame/Frame.cs +++ b/src/Backdash/Core/Frame/Frame.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Backdash.Core; @@ -75,6 +76,11 @@ namespace Backdash; /// public bool IsNull => Number is NullValue; + /// + /// Returns if the current frame is 0 + /// + public bool IsZero => Number is 0; + /// public int CompareTo(Frame other) => Number.CompareTo(other.Number); @@ -87,12 +93,22 @@ namespace Backdash; const string DefaultFormat = "(Frame 0);(Frame -#)"; /// - public string ToString(string? format, IFormatProvider? formatProvider) => + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string? format, + IFormatProvider? formatProvider + ) => Number.ToString(format ?? DefaultFormat, formatProvider); /// public override string ToString() => ToString(null, null); + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string format + ) => ToString(format, null); + /// public bool TryFormat( Span utf8Destination, out int bytesWritten, diff --git a/src/Backdash/Core/Json/InternalJsonConverters.cs b/src/Backdash/Core/Json/InternalJsonConverters.cs index 3b8e12bd..f617e66e 100644 --- a/src/Backdash/Core/Json/InternalJsonConverters.cs +++ b/src/Backdash/Core/Json/InternalJsonConverters.cs @@ -11,7 +11,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial Unsafe.BitCast(reader.GetInt32()); public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => - writer.WriteNumberValue(Unsafe.As(ref Unsafe.AsRef(ref value))); + writer.WriteNumberValue(Unsafe.As(ref value)); } [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field)] @@ -25,7 +25,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial Unsafe.BitCast(reader.GetInt64()); public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => - writer.WriteNumberValue(Unsafe.As(ref Unsafe.AsRef(ref value))); + writer.WriteNumberValue(Unsafe.As(ref value)); } [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field)] diff --git a/src/Backdash/Data/ObjectPool.cs b/src/Backdash/Data/ObjectPool.cs index 97ff1e92..4e8907db 100644 --- a/src/Backdash/Data/ObjectPool.cs +++ b/src/Backdash/Data/ObjectPool.cs @@ -22,33 +22,37 @@ public interface IObjectPool /// /// Default object pool for types with empty constructor /// -public sealed class DefaultObjectPool : IObjectPool, IEnumerable where T : class, new() +public sealed class ObjectPool : IObjectPool, IEnumerable, IDisposable where T : class { - /// - /// Default object pool singleton. - /// - public static readonly IObjectPool Instance = new DefaultObjectPool(); - /// /// Maximum number of objects allowed in the pool /// - public readonly int MaxCapacity; // -1 to account for fastItem + public int Capacity { get; } // -1 to account for fastItem int numItems; + T? fastItem; readonly Stack items; readonly HashSet set; + readonly Func createFunc; + readonly Action? returnFunc; readonly IEqualityComparer comparer; - T? fastItem; /// - /// Instantiate new + /// Instantiate new /// - public DefaultObjectPool(int capacity = 100, IEqualityComparer? comparer = null) + public ObjectPool( + Func createFunc, + Action? returnFunc = null, + int? capacity = null, + IEqualityComparer? comparer = null + ) { - MaxCapacity = capacity - 1; + this.createFunc = createFunc; + this.returnFunc = returnFunc; this.comparer = comparer ?? ReferenceEqualityComparer.Instance; - items = new(MaxCapacity); - set = new(MaxCapacity, this.comparer); + Capacity = (capacity ?? 100) - 1; + items = new(Capacity); + set = new(Capacity, this.comparer); } bool Contains(T value) => comparer.Equals(fastItem, value) || set.Contains(value); @@ -65,7 +69,7 @@ public T Rent() } if (!items.TryPop(out item)) - return new(); + return createFunc(); numItems--; set.Remove(item); @@ -78,13 +82,15 @@ public bool Return(T value) ArgumentNullException.ThrowIfNull(value); if (Contains(value)) return true; + returnFunc?.Invoke(value); + if (fastItem is null) { fastItem = value; return true; } - if (numItems >= MaxCapacity) + if (numItems >= Capacity) return false; if (!set.Add(value)) return true; @@ -104,11 +110,34 @@ public void Clear() set.Clear(); } + /// + /// Preload pool items. + /// + public void WarmUp(int count) + { + List temp = []; + for (var i = 0; i < count; i++) temp.Add(Rent()); + foreach (var player in temp) Return(player); + } + /// /// Number of instances in the object pool /// public int Count => numItems + (fastItem is null ? 0 : 1); + /// + /// Dispose all disposable objects in the pool + /// + public void Dispose() + { + (fastItem as IDisposable)?.Dispose(); + while (items.TryPop(out var item)) + if (item is IDisposable disposable) + disposable.Dispose(); + + Clear(); + } + /// public Stack.Enumerator GetEnumerator() => items.GetEnumerator(); @@ -116,3 +145,32 @@ public void Clear() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + +/// +/// Factory for +/// +public static class ObjectPool +{ + /// + /// Create new instance of object pool for with constructor. + /// + public static ObjectPool Create(int? capacity = null, Action? returnWith = null) + where T : class, new() => + new(static () => new(), returnWith, capacity); + + /// + /// Create new instance of object pool for with constructor. + /// + public static ObjectPool Create(Action returnWith) where T : class, new() => + Create(null, returnWith); + + /// + /// Object pool singleton factory + /// + public static ObjectPool Singleton() where T : class, new() => SingletonWrapper.Instance; + + static class SingletonWrapper where T : class, new() + { + public static readonly ObjectPool Instance = Create(); + } +} diff --git a/src/Backdash/Network/Messages/ConsistencyCheckFail.cs b/src/Backdash/Network/Messages/ConsistencyCheckFail.cs new file mode 100644 index 00000000..ec366b3a --- /dev/null +++ b/src/Backdash/Network/Messages/ConsistencyCheckFail.cs @@ -0,0 +1,37 @@ +using Backdash.Core; +using Backdash.Serialization; + +namespace Backdash.Network.Messages; + +[Serializable] +record struct ConsistencyCheckFail : IUtf8SpanFormattable +{ + public Frame Frame; + public Checksum LocalChecksum; + public Checksum RemoteChecksum; + + public readonly void Serialize(in BinarySpanWriter writer) + { + writer.Write(in Frame); + writer.Write(in LocalChecksum); + writer.Write(in RemoteChecksum); + } + + public void Deserialize(in BinaryBufferReader reader) + { + Frame = reader.ReadFrame(); + LocalChecksum = reader.ReadChecksum(); + RemoteChecksum = reader.ReadChecksum(); + } + + public readonly bool TryFormat( + Span utf8Destination, + out int bytesWritten, ReadOnlySpan format, + IFormatProvider? provider + ) + { + bytesWritten = 0; + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); + return writer.Write(in Frame) && writer.Write(in LocalChecksum) && writer.Write(in RemoteChecksum); + } +} diff --git a/src/Backdash/Network/Messages/ConsistencyCheckReply.cs b/src/Backdash/Network/Messages/ConsistencyCheckReply.cs index d7619f2e..9660919f 100644 --- a/src/Backdash/Network/Messages/ConsistencyCheckReply.cs +++ b/src/Backdash/Network/Messages/ConsistencyCheckReply.cs @@ -7,7 +7,7 @@ namespace Backdash.Network.Messages; record struct ConsistencyCheckReply : IUtf8SpanFormattable { public Frame Frame; - public uint Checksum; + public Checksum Checksum; public readonly void Serialize(in BinarySpanWriter writer) { @@ -18,7 +18,7 @@ public readonly void Serialize(in BinarySpanWriter writer) public void Deserialize(in BinaryBufferReader reader) { Frame = reader.ReadFrame(); - Checksum = reader.ReadUInt32(); + Checksum = reader.ReadChecksum(); } public readonly bool TryFormat( @@ -28,7 +28,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Frame) && writer.Write(in Checksum); } } diff --git a/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs b/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs index b3d56ffd..9dea2455 100644 --- a/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs +++ b/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs @@ -21,7 +21,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Frame); } } diff --git a/src/Backdash/Network/Messages/InputAck.cs b/src/Backdash/Network/Messages/InputAck.cs index efd3da17..bca45abc 100644 --- a/src/Backdash/Network/Messages/InputAck.cs +++ b/src/Backdash/Network/Messages/InputAck.cs @@ -19,7 +19,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in AckFrame); } } diff --git a/src/Backdash/Network/Messages/InputMessage.cs b/src/Backdash/Network/Messages/InputMessage.cs index 9e868758..acf40086 100644 --- a/src/Backdash/Network/Messages/InputMessage.cs +++ b/src/Backdash/Network/Messages/InputMessage.cs @@ -78,7 +78,7 @@ public readonly bool TryFormat( ReadOnlySpan format, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in StartFrame) && writer.Write(in AckFrame) && writer.Write(in NumBits); } diff --git a/src/Backdash/Network/Messages/MessageType.cs b/src/Backdash/Network/Messages/MessageType.cs index 8b851042..a3aed3d3 100644 --- a/src/Backdash/Network/Messages/MessageType.cs +++ b/src/Backdash/Network/Messages/MessageType.cs @@ -13,4 +13,5 @@ enum MessageType : ushort InputAck, ConsistencyCheckRequest, ConsistencyCheckReply, + ConsistencyCheckFail, } diff --git a/src/Backdash/Network/Messages/ProtocolMessage.cs b/src/Backdash/Network/Messages/ProtocolMessage.cs index 3cc0ac3c..3b72e58e 100644 --- a/src/Backdash/Network/Messages/ProtocolMessage.cs +++ b/src/Backdash/Network/Messages/ProtocolMessage.cs @@ -34,6 +34,9 @@ struct ProtocolMessage(MessageType type = MessageType.Unknown) : IEquatable SyncReply.ToString(), MessageType.ConsistencyCheckRequest => ConsistencyCheckRequest.ToString(), MessageType.ConsistencyCheckReply => ConsistencyCheckReply.ToString(), + MessageType.ConsistencyCheckFail => ConsistencyCheckFail.ToString(), MessageType.Input => Input.ToString(), MessageType.QualityReport => QualityReport.ToString(), MessageType.QualityReply => QualityReply.ToString(), @@ -148,6 +158,7 @@ public readonly bool TryFormat( MessageType.SyncReply => writer.Write(in SyncReply), MessageType.ConsistencyCheckRequest => writer.Write(in ConsistencyCheckRequest), MessageType.ConsistencyCheckReply => writer.Write(in ConsistencyCheckReply), + MessageType.ConsistencyCheckFail => writer.Write(in ConsistencyCheckFail), MessageType.QualityReply => writer.Write(in QualityReply), MessageType.QualityReport => writer.Write(in QualityReport), MessageType.InputAck => writer.Write(in InputAck), @@ -165,6 +176,7 @@ public readonly bool Equals(in ProtocolMessage other) => MessageType.SyncReply => SyncReply.Equals(other.SyncReply), MessageType.ConsistencyCheckRequest => ConsistencyCheckRequest.Equals(other.ConsistencyCheckRequest), MessageType.ConsistencyCheckReply => ConsistencyCheckReply.Equals(other.ConsistencyCheckReply), + MessageType.ConsistencyCheckFail => ConsistencyCheckFail.Equals(other.ConsistencyCheckFail), MessageType.Input => Input.Equals(in other.Input), MessageType.QualityReport => QualityReport.Equals(other.QualityReport), MessageType.QualityReply => QualityReply.Equals(other.QualityReply), diff --git a/src/Backdash/Network/Messages/QualityReply.cs b/src/Backdash/Network/Messages/QualityReply.cs index 59392819..74b89912 100644 --- a/src/Backdash/Network/Messages/QualityReply.cs +++ b/src/Backdash/Network/Messages/QualityReply.cs @@ -18,7 +18,7 @@ public readonly bool TryFormat(Span utf8Destination, out int bytesWritten, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Pong); } } diff --git a/src/Backdash/Network/Messages/QualityReport.cs b/src/Backdash/Network/Messages/QualityReport.cs index edc5a183..7e6f700b 100644 --- a/src/Backdash/Network/Messages/QualityReport.cs +++ b/src/Backdash/Network/Messages/QualityReport.cs @@ -26,7 +26,7 @@ public readonly bool TryFormat(Span utf8Destination, out int bytesWritten, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in FrameAdvantage) && writer.Write(in Ping); } } diff --git a/src/Backdash/Network/Messages/SyncReply.cs b/src/Backdash/Network/Messages/SyncReply.cs index db2a17e9..7531c863 100644 --- a/src/Backdash/Network/Messages/SyncReply.cs +++ b/src/Backdash/Network/Messages/SyncReply.cs @@ -29,7 +29,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in RandomReply) && writer.Write(in Pong); } } diff --git a/src/Backdash/Network/Messages/SyncRequest.cs b/src/Backdash/Network/Messages/SyncRequest.cs index 1c5fa986..f135cf51 100644 --- a/src/Backdash/Network/Messages/SyncRequest.cs +++ b/src/Backdash/Network/Messages/SyncRequest.cs @@ -29,7 +29,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in RandomRequest) && writer.Write(in Ping); } } diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index 52b24b9a..c63d1edd 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -378,7 +378,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) state.Consistency.AskedFrame = new(checkFrame); state.Consistency.AskedChecksum = checksumStore.Get(state.Consistency.AskedFrame); - if (state.Consistency.AskedFrame.IsNull || state.Consistency.AskedChecksum is 0) + if (state.Consistency.AskedFrame.IsNull || state.Consistency.AskedChecksum.IsEmpty) return; if (state.Consistency.LastCheck is 0) @@ -394,7 +394,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) } logger.Write(LogLevel.Debug, - $"Begin consistency-check request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum:x8}"); + $"Begin consistency-check request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum}"); outbox .SendMessage(new(MessageType.ConsistencyCheckRequest) diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 85d30e2e..d78210bc 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -103,7 +103,8 @@ bool HandleMessage(ref readonly ProtocolMessage message, out ProtocolMessage rep MessageType.QualityReply => OnQualityReply(in message), MessageType.InputAck => OnInputAck(in message), MessageType.ConsistencyCheckRequest => OnConsistencyCheckRequest(in message, ref replyMsg), - MessageType.ConsistencyCheckReply => OnConsistencyCheckReply(in message), + MessageType.ConsistencyCheckReply => OnConsistencyCheckReply(in message, ref replyMsg), + MessageType.ConsistencyCheckFail => OnConsistencyCheckFail(in message), MessageType.KeepAlive => true, MessageType.Unknown => throw new NetcodeException($"Unknown UDP protocol message received: {message.Header.Type}"), @@ -289,24 +290,48 @@ public bool OnSyncRequest(ref readonly ProtocolMessage msg, ref ProtocolMessage return true; } - bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) + bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) + { + var checkFrame = message.ConsistencyCheckRequest.Frame; + var checksum = checksumStore.Get(checkFrame); + + logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum})"); + + if (checksum.IsEmpty) + { + logger.Write(LogLevel.Warning, $"Unable to find requested local checksum for {checkFrame}"); + return false; + } + + replyMsg.Header.Type = MessageType.ConsistencyCheckReply; + replyMsg.ConsistencyCheckReply.Frame = checkFrame; + replyMsg.ConsistencyCheckReply.Checksum = checksum; + return true; + } + + bool OnConsistencyCheckReply(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) { var checkFrame = message.ConsistencyCheckReply.Frame; var checksum = message.ConsistencyCheckReply.Checksum; var localChecksum = state.Consistency.AskedChecksum; - logger.Write(LogLevel.Debug, $"Reply consistency-check for {checkFrame} #{checksum:x8}"); + logger.Write(LogLevel.Debug, $"Reply consistency-check for {checkFrame} #{checksum}"); - if (state.Consistency.AskedFrame != checkFrame || localChecksum is 0 || checksum is 0) + if (state.Consistency.AskedFrame != checkFrame || localChecksum.IsEmpty || checksum.IsEmpty) { - logger.Write(LogLevel.Warning, $"Unable to find reply local checksum #{checksum:x8} for {checkFrame}"); + logger.Write(LogLevel.Warning, $"Unable to find reply local checksum #{checksum} for {checkFrame}"); return false; } if (localChecksum != checksum) { logger.Write(LogLevel.Error, - $"Invalid remote checksum on frame {checkFrame}, {localChecksum:x8} != {checksum:x8}"); + $"Invalid remote checksum on frame {checkFrame}, {localChecksum} != {checksum}"); + + replyMsg.Header.Type = MessageType.ConsistencyCheckFail; + replyMsg.ConsistencyCheckFail.Frame = checkFrame; + replyMsg.ConsistencyCheckFail.RemoteChecksum = checksum; + replyMsg.ConsistencyCheckFail.LocalChecksum = localChecksum; networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) { @@ -318,33 +343,33 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) } ); - return false; + return true; } - logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum:x8}"); + logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum}"); state.Consistency.LastCheck = Stopwatch.GetTimestamp(); state.Consistency.AskedFrame = Frame.Null; - state.Consistency.AskedChecksum = 0; - + state.Consistency.AskedChecksum = Checksum.Empty; return true; } - bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) + bool OnConsistencyCheckFail(ref readonly ProtocolMessage message) { - var checkFrame = message.ConsistencyCheckRequest.Frame; - var checksum = checksumStore.Get(checkFrame); - - logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum:x8})"); + var body = message.ConsistencyCheckFail; + logger.Write(LogLevel.Warning, $"Failure in consistency-check for {body}"); - if (checksum is 0) + var localChecksum = body.RemoteChecksum; + var remoteChecksum = body.LocalChecksum; + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) { - logger.Write(LogLevel.Warning, $"Unable to find requested local checksum for {checkFrame}"); - return false; + ChecksumMismatch = new( + MismatchFrame: body.Frame, + LocalChecksum: localChecksum, + RemoteChecksum: remoteChecksum + ), } + ); - replyMsg.Header.Type = MessageType.ConsistencyCheckReply; - replyMsg.ConsistencyCheckReply.Frame = checkFrame; - replyMsg.ConsistencyCheckReply.Checksum = checksum; return true; } } diff --git a/src/Backdash/Network/Protocol/ProtocolState.cs b/src/Backdash/Network/Protocol/ProtocolState.cs index 6f115676..8a543875 100644 --- a/src/Backdash/Network/Protocol/ProtocolState.cs +++ b/src/Backdash/Network/Protocol/ProtocolState.cs @@ -37,7 +37,7 @@ public sealed class ConsistencyState { public long LastCheck; public Frame AskedFrame; - public uint AskedChecksum; + public Checksum AskedChecksum; } public sealed class AdvantageState diff --git a/src/Backdash/Options/NetcodeOptions.cs b/src/Backdash/Options/NetcodeOptions.cs index 0b734f8e..9d07eb09 100644 --- a/src/Backdash/Options/NetcodeOptions.cs +++ b/src/Backdash/Options/NetcodeOptions.cs @@ -83,7 +83,7 @@ internal EndiannessSerializer.INumberSerializer GetEndiannessNumberStateSerializ public int InputDelayFrames { get; set; } = 2; /// - /// Value to override the total number of in state store. + /// Value to override the total number of in state store. /// /// Defaults to + /// diff --git a/src/Backdash/Player/PeerEvent.cs b/src/Backdash/Player/PeerEvent.cs index 5625491f..991e29a3 100644 --- a/src/Backdash/Player/PeerEvent.cs +++ b/src/Backdash/Player/PeerEvent.cs @@ -172,4 +172,8 @@ public bool TryFormat( /// /// Data for event. /// -public readonly record struct ChecksumMismatchEventInfo(Frame MismatchFrame, uint LocalChecksum, uint RemoteChecksum); +public readonly record struct ChecksumMismatchEventInfo( + Frame MismatchFrame, + Checksum LocalChecksum, + Checksum RemoteChecksum +); diff --git a/src/Backdash/Serialization/BinaryBufferReader.cs b/src/Backdash/Serialization/BinaryBufferReader.cs index 7d764f88..da83c121 100644 --- a/src/Backdash/Serialization/BinaryBufferReader.cs +++ b/src/Backdash/Serialization/BinaryBufferReader.cs @@ -274,6 +274,12 @@ public DateTimeOffset ReadDateTimeOffset() /// public Frame? ReadNullableFrame() => ReadBoolean() ? ReadFrame() : null; + /// Reads single from the buffer. + public Checksum ReadChecksum() => ReadAsUInt32(); + + /// Reads single from the buffer. + public Checksum? ReadNullableChecksum() => ReadBoolean() ? ReadChecksum() : null; + /// Reads an unmanaged struct from the buffer. public void ReadStruct(ref T value) where T : unmanaged { @@ -432,7 +438,7 @@ public void ReadNullable(ref T? value, IObjectPool pool, bool forceReturn /// Reads a nullable from the buffer. /// A nullable reference type that implements . public void ReadNullable(ref T? value, bool forceReturn = true) where T : class, IBinarySerializable, new() => - ReadNullable(ref value, DefaultObjectPool.Instance, forceReturn); + ReadNullable(ref value, ObjectPool.Singleton(), forceReturn); /// Reads a from the buffer. /// A value type that implements . @@ -504,7 +510,7 @@ public void Read(ref T? value, bool nullable, bool forceReturn = true) /// Reads a span of into buffer. /// A list of a reference type that implements . - public void Read(in Span values, in IObjectPool pool) where T : IBinarySerializable + public void Read(in Span values, IObjectPool pool) where T : IBinarySerializable { if (values.IsEmpty) return; ref var current = ref MemoryMarshal.GetReference(values); @@ -527,20 +533,20 @@ public void Read(in Span values, in IObjectPool pool) where T : IBinary /// A reference type that implements . [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Read(in T[] values, in IObjectPool pool) where T : class, IBinarySerializable => - Read(values.AsSpan(), in pool); + Read(values.AsSpan(), pool); /// Reads an array of into buffer. /// A reference that implements . public void Read(in List values, IObjectPool pool) where T : class, IBinarySerializable => - Read(GetListSpan(in values, pool), in pool); + Read(GetListSpan(in values, pool), pool); /// /// Reads a span of from the buffer. /// - /// + /// /// A reference that implements . public void Read(Span values) where T : class, IBinarySerializable, new() => - Read(values, in DefaultObjectPool.Instance); + Read(values, ObjectPool.Singleton()); /// /// Reads an array of from the buffer. @@ -557,7 +563,7 @@ public void Read(in List values, IObjectPool pool) where T : class, IBi /// /// A reference that implements . public void Read(List values) where T : class, IBinarySerializable, new() => - Read(GetListSpan(in values, in DefaultObjectPool.Instance)); + Read(GetListSpan(in values, ObjectPool.Singleton())); /// /// Reads a StringBuilder into from the buffer. @@ -702,6 +708,12 @@ public void Read(in StringBuilder values) /// public void Read(ref Frame? value) => value = ReadNullableFrame(); + /// + public void Read(ref Checksum value) => value = ReadChecksum(); + + /// + public void Read(ref Checksum? value) => value = ReadNullableChecksum(); + /// Reads a span of from buffer into . public void Read(in Span values) { diff --git a/src/Backdash/Serialization/BinarySpanWriter.cs b/src/Backdash/Serialization/BinarySpanWriter.cs index 82fa4f07..aeb53ac5 100644 --- a/src/Backdash/Serialization/BinarySpanWriter.cs +++ b/src/Backdash/Serialization/BinarySpanWriter.cs @@ -184,6 +184,9 @@ public void Write(in DateTimeOffset value) /// Writes single into buffer. public void Write(in Frame value) => WriteAsInt32(in value); + /// Writes single into buffer. + public void Write(in Checksum value) => WriteAsUInt32(in value); + /// Writes a span of into buffer. public void Write(in ReadOnlySpan value) { diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index a4277fe6..287888c3 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -75,7 +75,8 @@ SessionServices services public FrameSpan FramesBehind => FrameSpan.Zero; public FrameSpan RollbackFrames => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public IReadOnlySet GetPlayers() => addedPlayers; @@ -233,6 +234,7 @@ public void LoadSnapshot(StateSnapshot snapshot) { CurrentFrame = snapshot.Frame; DiscardInputsAfter(CurrentFrame); + stateStore.Seek(CurrentFrame); } var offset = 0; @@ -263,7 +265,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); + logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } public void SetFrameDelay(NetcodePlayer player, int delayInFrames) diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 1b97a1be..310f28c7 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -199,7 +199,8 @@ void Close() public FrameSpan FramesBehind => synchronizer.FramesBehind; public FrameSpan RollbackFrames => synchronizer.RollbackFrames; public bool IsInRollback => synchronizer.InRollback; - public SavedFrame GetCurrentSavedFrame() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.Store.Last(); + public SavedState? GetSavedState(Frame frame) => synchronizer.Store.Get(frame); public int NumberOfPlayers => addedPlayers.Count; public int NumberOfSpectators => addedSpectators.Count; public int LocalPort => udp.BindPort; diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index 2e23f560..ee4069ed 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -101,7 +101,8 @@ public void Close() public FrameSpan RollbackFrames => FrameSpan.Zero; public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public int NumberOfSpectators => 0; public int LocalPort => 0; @@ -216,7 +217,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); + logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } public bool LoadFrame(Frame frame) @@ -237,7 +238,7 @@ public bool LoadFrame(Frame frame) } logger.Write(LogLevel.Trace, - $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); var offset = 0; BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); callbacks.LoadState(frame, ref reader); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 43d810cb..1b47a6e5 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -158,7 +158,8 @@ public void Close() public FrameSpan RollbackFrames => FrameSpan.Zero; public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public INetcodeRandom Random => random; public INetcodeSessionHandler GetHandler() => callbacks; public int NumberOfPlayers { get; private set; } @@ -324,7 +325,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"spectator: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})."); + logger.Write(LogLevel.Trace, $"spectator: saved frame {nextState.Frame} (checksum: {nextState.Checksum})."); } public bool LoadFrame(Frame frame) @@ -341,7 +342,7 @@ public bool LoadFrame(Frame frame) return false; logger.Write(LogLevel.Trace, - $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); var offset = 0; BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index c3de6921..47f75155 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -18,7 +18,7 @@ sealed class SyncTestSession : INetcodeSession { readonly record struct SavedFrameBytes( Frame Frame, - uint Checksum, + Checksum Checksum, byte[] State, int StateSize, GameInput> Inputs @@ -130,7 +130,8 @@ public async ValueTask DisposeAsync() public ReadOnlySpan CurrentInputs => inputBuffer; public bool IsInRollback => synchronizer.InRollback; - public SavedFrame GetCurrentSavedFrame() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.Store.Last(); + public SavedState? GetSavedState(Frame frame) => synchronizer.Store.Get(frame); public IReadOnlySet GetPlayers() => addedPlayers.Count is 0 ? localPlayerFallback : addedPlayers.Keys.ToHashSet(); @@ -277,7 +278,20 @@ public bool LoadFrame(Frame frame) return synchronizer.TryLoadFrame(frame); } - public void LoadSnapshot(StateSnapshot snapshot) { } + public void LoadSnapshot(StateSnapshot snapshot) + { + if (snapshot.State is []) return; + var frame = CurrentFrame; + + if (snapshot.Frame.Number > 0) + { + frame = snapshot.Frame; + synchronizer.Store.Seek(frame); + } + + synchronizer.ApplyState(frame, snapshot.State); + savedFrames.Clear(); + } public void AdvanceFrame() { @@ -298,7 +312,7 @@ public void AdvanceFrame() // Hold onto the current frame in our queue of saved states. // We'll need the checksum later to verify that our replay of the same frame got the same results. - var lastSaved = synchronizer.GetLastSavedFrame(); + var lastSaved = synchronizer.Store.Last(); var stateBytes = ArrayPool.Shared.Rent(lastSaved.GameState.WrittenCount); lastSaved.GameState.WrittenSpan.CopyTo(stateBytes); @@ -336,12 +350,12 @@ public void AdvanceFrame() throw new NetcodeException(message); } - var last = synchronizer.GetLastSavedFrame(); + var last = synchronizer.Store.Last(); if (current.Checksum != last.Checksum) HandleDesync(frame, current, last); else logger.Write(LogLevel.Trace, - $"Checksum #{last.Checksum:x8} for frame {current.Frame.Number} matches"); + $"Checksum #{last.Checksum} for frame {current.Frame.Number} matches"); } finally { @@ -353,11 +367,11 @@ public void AdvanceFrame() inRollback = false; } - void HandleDesync(Frame frame, SavedFrameBytes current, SavedFrame previous) + void HandleDesync(Frame frame, SavedFrameBytes current, SavedState previous) { const LogLevel level = LogLevel.Error; var message = - $"Checksum for frame {frame} does NOT match: (#{previous.Checksum:x8} != #{current.Checksum:x8})\n"; + $"Checksum for frame {frame} does NOT match: (#{previous.Checksum} != #{current.Checksum})\n"; logger.Write(LogLevel.Error, message); @@ -389,15 +403,15 @@ void HandleDesync(Frame frame, SavedFrameBytes current, SavedFrame previous) void LogSaveState(LogLevel level, string description, string body, - uint checksum, Frame frame, + Checksum checksum, Frame frame, object? extra = null ) { if (!logStateOnDesync) return; logger.Write(level, $"=> SAVED [{description}] (Frame {frame}{(extra is not null ? $" / {extra}" : "")})"); - logger.Write(level, $"== START STATE #{checksum:x8} =="); + logger.Write(level, $"== START STATE #{checksum} =="); LogText(level, body); - logger.Write(level, $"== END STATE #{checksum:x8} ==\n"); + logger.Write(level, $"== END STATE #{checksum} ==\n"); } void LogText(LogLevel level, string text) diff --git a/src/Backdash/Session/INetcodeSession.cs b/src/Backdash/Session/INetcodeSession.cs index 7cf78d19..d829ec89 100644 --- a/src/Backdash/Session/INetcodeSession.cs +++ b/src/Backdash/Session/INetcodeSession.cs @@ -75,24 +75,34 @@ public interface INetcodeSessionInfo Endianness InputSerializationEndianness { get; } /// - /// Returns the last saved state. + /// Returns the checksum of the current saved state. /// - SavedFrame GetCurrentSavedFrame(); + Checksum CurrentChecksum => GetSavedState().Checksum; /// - /// Returns the checksum of the current saved state. + /// Returns the size of the current saved state. /// - uint CurrentChecksum => GetCurrentSavedFrame().Checksum; + ByteSize CurrentStateSize => GetSavedState().Size; /// - /// Returns the size of the current saved state. + /// Returns the current saved state for if exists, otherwise. + /// + SavedState? GetSavedState(Frame frame); + + /// + /// Returns the current saved state. + /// + SavedState GetSavedState(); + + /// + /// Returns the current state snapshot for if exists, otherwise. /// - ByteSize CurrentStateSize => GetCurrentSavedFrame().Size; + StateSnapshot? GetStateSnapshot(Frame frame) => GetSavedState(frame)?.ToSnapshot(); /// - /// Returns the last saved state snapshot. + /// Returns the current saved state snapshot. /// - StateSnapshot CurrentStateSnapshot() => GetCurrentSavedFrame().ToSnapshot(); + StateSnapshot GetStateSnapshot() => GetSavedState().ToSnapshot(); } /// @@ -289,6 +299,9 @@ bool TryGetPlayerByCustomId(int customId, [NotNullWhen(true)] out NetcodePlayer? /// NetcodePlayer? GetPlayerByCustomId(int customId) => TryGetPlayerByCustomId(customId, out var player) ? player : null; + + /// + NetcodePlayer? GetLocalPlayer() => TryGetLocalPlayer(out var player) ? player : null; } /// diff --git a/src/Backdash/Session/NetcodeSessionExtensions.cs b/src/Backdash/Session/NetcodeSessionExtensions.cs index d9184925..c34d8cba 100644 --- a/src/Backdash/Session/NetcodeSessionExtensions.cs +++ b/src/Backdash/Session/NetcodeSessionExtensions.cs @@ -11,25 +11,27 @@ public static class NetcodeSessionExtensions /// /// Returns the current state string representation /// - public static string GetStateString(this INetcodeSession @this, IStateStringParser? parser = null) + public static StatePreview GetStateString(this INetcodeSession @this, IStateStringParser? parser = null) where T : unmanaged { - var state = @this.GetCurrentSavedFrame(); + var state = @this.GetSavedState(); var currentBytes = state.GameState.WrittenSpan; - return @this.GetStateString(state.Frame, currentBytes, parser); + var text = @this.GetStateString(state.Frame, currentBytes, parser); + return new(state.Frame, state.Checksum, text); } /// /// Returns string representation for given /// - public static string GetStateString( + public static StatePreview GetStateString( this INetcodeSession @this, StateSnapshot state, IStateStringParser? parser = null ) where T : unmanaged { var stateBytes = state.State.AsSpan(0, (int)state.Size); - return @this.GetStateString(state.Frame, stateBytes, parser); + var text = @this.GetStateString(state.Frame, stateBytes, parser); + return new(state.Frame, state.Checksum, text); } /// @@ -48,4 +50,26 @@ static string GetStateString( var stateObject = @this.GetHandler().CreateState(frame, ref reader); return parser.GetStateString(frame, in reader, stateObject); } + + /// + /// Enumerate all valid state saved snapshots in descending order + /// + public static IEnumerable EnumerateSnapshots(this INetcodeSession @this, Frame? frame = null) + where T : unmanaged + { + StateSnapshot? next = frame.HasValue ? @this.GetStateSnapshot(frame.Value) : @this.GetStateSnapshot(); + while (next is not null) + { + yield return next; + next = @this.GetStateSnapshot(next.Frame.Previous()); + } + } + + /// + /// Enumerate string representation for all saved states in descending order + /// + public static IEnumerable EnumerateStateStrings( + this INetcodeSession @this, Frame? frame = null, IStateStringParser? parser = null) + where T : unmanaged => + @this.EnumerateSnapshots(frame).Select(s => @this.GetStateString(s, parser)); } diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 2610b8da..2d05902a 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -16,7 +16,6 @@ sealed class Synchronizer where TInput : unmanaged readonly NetcodeOptions options; readonly Logger logger; readonly IReadOnlyCollection players; - readonly IStateStore stateStore; readonly IChecksumProvider checksumProvider; readonly ChecksumStore checksumStore; readonly ConnectionsState localConnections; @@ -28,8 +27,6 @@ sealed class Synchronizer where TInput : unmanaged bool reachedPredictionBarrier; int NumberOfPlayers => players.Count; - readonly EndiannessSerializer.INumberSerializer endianness; - public Synchronizer( NetcodeOptions options, Logger logger, @@ -44,25 +41,27 @@ public Synchronizer( this.options = options; this.logger = logger; this.players = players; - this.stateStore = stateStore; + this.Store = stateStore; this.checksumProvider = checksumProvider; this.localConnections = localConnections; this.inputComparer = inputComparer ?? EqualityComparer.Default; this.checksumStore = checksumStore; inputQueues = new(2); - endianness = options.GetEndiannessNumberStateSerializer(); + NumberSerializer = options.GetEndiannessNumberStateSerializer(); var saveBufferSize = options.TotalSavedFramesAllowed; stateStore.Initialize(saveBufferSize); } public bool InRollback { get; private set; } + public IStateStore Store { get; } + public EndiannessSerializer.INumberSerializer NumberSerializer { get; } float rollbackFrameCounter; public Frame CurrentFrame => currentFrame; - public EndiannessSerializer.INumberSerializer NumberSerializer => endianness; - public Endianness SerializationEndianness => endianness.Endianness; + + public Endianness SerializationEndianness => NumberSerializer.Endianness; public FrameSpan FramesBehind => new(currentFrame.Number - lastConfirmedFrame.Number); public FrameSpan RollbackFrames => new((int)Math.Round(rollbackFrameCounter)); @@ -213,21 +212,25 @@ public bool TryLoadFrame(Frame frame) return true; } - if (!stateStore.TryLoad(frame, out var savedFrame)) + if (!Store.TryLoad(frame, out var savedFrame)) return false; logger.Write(LogLevel.Information, - $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); - var offset = 0; - BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); + ApplyState(savedFrame.Frame, savedFrame.GameState.WrittenSpan); + return true; + } + public void ApplyState(Frame frame, ReadOnlySpan state) + { + var offset = 0; + BinaryBufferReader reader = new(state, ref offset, NumberSerializer); Callbacks.LoadState(frame, ref reader); // Reset frame count and the head of the state ring-buffer to point in // advance of the current frame (as if we had just finished executing it). - currentFrame = savedFrame.Frame; - return true; + currentFrame = frame; } public void LoadFrame(Frame frame) @@ -236,20 +239,18 @@ public void LoadFrame(Frame frame) throw new NetcodeException($"Save state not found for frame {frame.Number}"); } - public SavedFrame GetLastSavedFrame() => stateStore.Last(); - public void SaveCurrentFrame() { - ref var nextState = ref stateStore.Next(); + ref var nextState = ref Store.Next(); - BinaryBufferWriter writer = new(nextState.GameState, endianness); + BinaryBufferWriter writer = new(nextState.GameState, NumberSerializer); 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})"); + Store.Advance(); + logger.Write(LogLevel.Trace, $"sync: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } bool CheckSimulationConsistency(out Frame seekTo) diff --git a/src/Backdash/Synchronizing/State/ChecksumProvider.cs b/src/Backdash/Synchronizing/State/ChecksumProvider.cs index d4cc2102..b89bdb61 100644 --- a/src/Backdash/Synchronizing/State/ChecksumProvider.cs +++ b/src/Backdash/Synchronizing/State/ChecksumProvider.cs @@ -10,7 +10,7 @@ public interface IChecksumProvider /// /// /// checksum value - uint Compute(ReadOnlySpan data); + Checksum Compute(ReadOnlySpan data); } /// @@ -18,7 +18,7 @@ public interface IChecksumProvider sealed class DelegateChecksumProvider(ChecksumDelegate compute) : IChecksumProvider { - public uint Compute(ReadOnlySpan data) => compute(data); + public Checksum Compute(ReadOnlySpan data) => (Checksum)compute(data); } /// @@ -27,7 +27,7 @@ sealed class DelegateChecksumProvider(ChecksumDelegate compute) : IChecksumProvi public class EmptyChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) => 0; + public Checksum Compute(ReadOnlySpan data) => Checksum.Empty; } /// @@ -39,9 +39,9 @@ public sealed class Fletcher32ChecksumProvider : IChecksumProvider const int BlockSize = 360; /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -77,6 +77,6 @@ public unsafe uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } diff --git a/src/Backdash/Synchronizing/State/ChecksumStore.cs b/src/Backdash/Synchronizing/State/ChecksumStore.cs index cf5ed902..b75d997d 100644 --- a/src/Backdash/Synchronizing/State/ChecksumStore.cs +++ b/src/Backdash/Synchronizing/State/ChecksumStore.cs @@ -10,22 +10,22 @@ public ChecksumStore(int size) data = new Entry[size]; } - public void Add(Frame frame, uint checksum) + public void Add(Frame frame, Checksum checksum) { ref var entry = ref data[frame.Number % data.Length]; entry.Frame = frame; entry.Checksum = checksum; } - public uint Get(Frame frame) + public Checksum Get(Frame frame) { var entry = data[frame.Number % data.Length]; - return entry.Frame == frame ? entry.Checksum : 0; + return entry.Frame == frame ? entry.Checksum : Checksum.Empty; } struct Entry { public Frame Frame; - public uint Checksum; + public Checksum Checksum; } } diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index df938346..e01e6bf1 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -12,18 +12,21 @@ namespace Backdash.Synchronizing.State; public sealed class DefaultStateStore(int hintSize) : IStateStore { int head; - SavedFrame[] savedStates = []; + SavedState[] savedStates = []; /// public void Initialize(int saveCount) { - savedStates = new SavedFrame[saveCount]; + savedStates = new SavedState[saveCount]; for (var i = 0; i < saveCount; i++) - savedStates[i] = new(Frame.Null, new(hintSize), 0); + savedStates[i] = new(Frame.Null, new(hintSize), Checksum.Empty); } /// - public ref SavedFrame Next() + public void Advance() => head = (head + 1) % savedStates.Length; + + /// + public ref SavedState Next() { ref var result = ref savedStates[head]; result.GameState.ResetWrittenCount(); @@ -31,20 +34,40 @@ public ref SavedFrame Next() } /// - public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFrame) + public SavedState Last() { - var i = 0; - var span = savedStates.AsSpan(); - ref var current = ref MemoryMarshal.GetReference(span); - ref var limit = ref Unsafe.Add(ref current, span.Length); + var i = head - 1; + var index = i < 0 ? savedStates.Length - 1 : i; + return savedStates[index]; + } + + bool IsInRange(Frame frame) + { + if (frame.IsNull) return false; + var last = Last().Frame; + if (last.Number <= 0) return true; + return frame.Number <= last.Number; + } + + /// + public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result) + { + if (!IsInRange(frame)) + { + result = null; + return false; + } + var i = 0; + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); while (Unsafe.IsAddressLessThan(ref current, ref limit)) { if (current.Frame.Number == frame.Number) { head = i; Advance(); - savedFrame = current; + result = current; return true; } @@ -52,18 +75,49 @@ public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFram current = ref Unsafe.Add(ref current, 1)!; } - savedFrame = null; + result = null; return false; } /// - public SavedFrame Last() + public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result) { - var i = head - 1; - var index = i < 0 ? savedStates.Length - 1 : i; - return savedStates[index]; + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); + while (Unsafe.IsAddressLessThan(ref current, ref limit)) + { + if (current.Frame.Number == frame.Number) + { + result = current; + return true; + } + + current = ref Unsafe.Add(ref current, 1)!; + } + + result = null; + return false; } /// - public void Advance() => head = (head + 1) % savedStates.Length; + public bool Seek(Frame frame) + { + if (!IsInRange(frame)) return false; + var i = 0; + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); + while (Unsafe.IsAddressLessThan(ref current, ref limit)) + { + if (current.Frame == frame) + { + head = i; + return true; + } + + i++; + current = ref Unsafe.Add(ref current, 1)!; + } + + return false; + } } diff --git a/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs b/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs index e68dc77f..65ae6240 100644 --- a/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs +++ b/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs @@ -19,7 +19,7 @@ public interface IStateDesyncHandler public readonly ref struct DesyncState( string value, ref readonly BinaryBufferReader reader, - uint checksum, + Checksum checksum, object? state ) { @@ -34,7 +34,7 @@ public readonly ref struct DesyncState( public readonly BinaryBufferReader Reader = reader; /// State checksum value - public readonly uint Checksum = checksum; + public readonly Checksum Checksum = checksum; /// public override string ToString() => Value; diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index c47d237b..3e52f9a2 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -14,23 +14,39 @@ public interface IStateStore void Initialize(int saveCount); /// - /// Try loads a for . + /// Try to load the for . /// /// true if the frame was found, false otherwise - bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFrame); + bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result); /// - /// Returns last . + /// Try to read the for . /// - SavedFrame Last(); + /// true if the frame was found, false otherwise + bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result); + + /// + /// Try set the state pointer to the first entry. + /// + bool Seek(Frame frame); + + /// + /// Returns last . + /// + SavedState Last(); /// - /// Returns next writable . + /// Returns next writable . /// - ref SavedFrame Next(); + ref SavedState Next(); /// /// Advance the store pointer /// void Advance(); + + /// + /// Return a for if exists, otherwise. + /// + SavedState? Get(Frame frame) => TryGet(frame, out var savedState) ? savedState : null; } diff --git a/src/Backdash/Synchronizing/State/SavedFrame.cs b/src/Backdash/Synchronizing/State/SavedState.cs similarity index 76% rename from src/Backdash/Synchronizing/State/SavedFrame.cs rename to src/Backdash/Synchronizing/State/SavedState.cs index bae5676f..066b61cc 100644 --- a/src/Backdash/Synchronizing/State/SavedFrame.cs +++ b/src/Backdash/Synchronizing/State/SavedState.cs @@ -9,13 +9,13 @@ namespace Backdash.Synchronizing.State; /// Saved frame number /// Game state on /// Checksum of state -public sealed record SavedFrame(Frame Frame, ArrayBufferWriter GameState, uint Checksum) +public sealed record SavedState(Frame Frame, ArrayBufferWriter GameState, Checksum Checksum) { /// Saved frame number public Frame Frame = Frame; /// Saved checksum - public uint Checksum = Checksum; + public Checksum Checksum = Checksum; /// Saved game state public readonly ArrayBufferWriter GameState = GameState; @@ -24,5 +24,5 @@ public sealed record SavedFrame(Frame Frame, ArrayBufferWriter GameState, public ByteSize Size => ByteSize.FromBytes(GameState.WrittenCount); /// Returns a snapshot of the current saved state - public StateSnapshot ToSnapshot() => new(Frame, GameState.WrittenSpan.ToArray()); + public StateSnapshot ToSnapshot() => new(Frame, Checksum, GameState.WrittenSpan.ToArray()); } diff --git a/src/Backdash/Synchronizing/State/StateSnapshot.cs b/src/Backdash/Synchronizing/State/StateSnapshot.cs index ebd71183..b6518152 100644 --- a/src/Backdash/Synchronizing/State/StateSnapshot.cs +++ b/src/Backdash/Synchronizing/State/StateSnapshot.cs @@ -7,16 +7,20 @@ namespace Backdash.Synchronizing.State; /// /// Saved frame number /// Game state on +/// Checksum of state [Serializable] -public sealed class StateSnapshot(Frame frame, byte[] state) +public sealed class StateSnapshot(Frame frame, Checksum checksum, byte[] state) { /// /// Creates an empty state snapshot. /// - public StateSnapshot() : this(Frame.Null, []) { } + public StateSnapshot() : this(Frame.Null, Checksum.Empty, []) { } /// Saved frame number - public readonly Frame Frame = frame; + public Frame Frame = frame; + + /// Saved state checksum + public readonly Checksum Checksum = checksum; /// Saved game state public readonly byte[] State = state; @@ -24,3 +28,27 @@ public StateSnapshot() : this(Frame.Null, []) { } /// Saved state size public ByteSize Size => ByteSize.FromBytes(State.Length); } + +/// +/// A specific frame saved state text representation. +/// +/// Saved frame number +/// Game state string for +/// Checksum of state +[Serializable] +public sealed class StatePreview(Frame frame, Checksum checksum, string state) +{ + /// + /// Creates an empty state snapshot. + /// + public StatePreview() : this(Frame.Null, Checksum.Empty, string.Empty) { } + + /// Saved frame number + public readonly Frame Frame = frame; + + /// State checksum + public readonly Checksum Checksum = checksum; + + /// Game state text + public readonly string State = state; +} diff --git a/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs b/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs index d2140e8e..d4fa06fa 100644 --- a/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs @@ -35,6 +35,13 @@ public class ByteSizeJsonSerializationTests() : [Fact] public void ShouldSerialize() => SerializeTest(); } +public class ChecksumJsonSerializationTests() : + BaseJsonConverterTests(Gen.Checksum, x => $"\"{x.Value:x8}\"") +{ + [Fact] public void ShouldDeserialize() => DeserializeTest(); + [Fact] public void ShouldSerialize() => SerializeTest(); +} + public class CircularBufferJsonSerializationTests() : BaseJsonConverterTests>( () => CircularBuffer.CreateFrom([10, 99, 22, 11, 77]), diff --git a/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs b/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs index 5c1e0c52..89bbbd8e 100644 --- a/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs @@ -75,4 +75,11 @@ internal bool ConsistencyCheckReplySerialize(ConsistencyCheckReply value) => (ref ConsistencyCheckReply v, BinarySpanWriter w) => v.Serialize(w), (ref ConsistencyCheckReply v, BinaryBufferReader r) => v.Deserialize(r) ); + + [PropertyTest] + internal bool ConsistencyCheckFailSerialize(ConsistencyCheckFail value) => + AssertThat.Serialization.IsValid(ref value, + (ref ConsistencyCheckFail v, BinarySpanWriter w) => v.Serialize(w), + (ref ConsistencyCheckFail v, BinaryBufferReader r) => v.Deserialize(r) + ); } diff --git a/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs b/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs index 404a3c22..d43ac8bd 100644 --- a/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs @@ -404,8 +404,7 @@ public bool SerializableObject(SimpleRefData value, SimpleRefData result, Endian return value == result; } - static DefaultObjectPool DataPool => - (DefaultObjectPool)DefaultObjectPool.Instance; + static ObjectPool DataPool => ObjectPool.Singleton(); [PropertyTest] public bool SerializableNullableObjectToObject( diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index c78c0f02..b351a131 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -15,7 +15,7 @@ public void ShouldInitializeCorrectly() ref var currentState = ref store.Next(); currentState.Frame = Frame.One; - currentState.Checksum = 0; + currentState.Checksum = Checksum.Empty; var gameState = GameState.CreateRandom(); GameStateSerializer.Shared.Serialize(new(currentState.GameState), in gameState); diff --git a/tests/Backdash.Tests/TestUtils/Gen.cs b/tests/Backdash.Tests/TestUtils/Gen.cs index 215cddaa..8ef2458f 100644 --- a/tests/Backdash.Tests/TestUtils/Gen.cs +++ b/tests/Backdash.Tests/TestUtils/Gen.cs @@ -12,6 +12,7 @@ static class Gen public static Vector3 Vector3() => new(Random.Float(), Random.Float(), Random.Float()); public static PeerAddress Peer() => Faker.Internet.IpEndPoint(); public static Frame Frame() => new(Faker.Random.Int(0)); + public static Checksum Checksum() => new(Faker.Random.UInt()); public static FrameSpan FrameSpan() => new(Faker.Random.Int(0)); public static FrameRange FrameRange() => new(Faker.Random.Int(0), Faker.Random.Int(0)); public static ByteSize ByteSize() => new(Faker.Random.Long(0)); diff --git a/tests/Backdash.Tests/TestUtils/TestGenerators.cs b/tests/Backdash.Tests/TestUtils/TestGenerators.cs index 00ef77df..786ff58e 100644 --- a/tests/Backdash.Tests/TestUtils/TestGenerators.cs +++ b/tests/Backdash.Tests/TestUtils/TestGenerators.cs @@ -60,6 +60,11 @@ public static Arbitrary FrameGenerator() => .Select(x => new Frame(x.Item)) .ToArbitrary(); + public static Arbitrary ChecksumGenerator() => + ArbMap.Default.GeneratorFor() + .Select(x => new Checksum(x)) + .ToArbitrary(); + public static Arbitrary TimeOnlyGenerator() => ArbMap.Default.GeneratorFor() .Where(x => x >= TimeOnly.MinValue.Ticks && x <= TimeOnly.MaxValue.Ticks) @@ -242,7 +247,7 @@ from frame in Generate() public static Arbitrary ConsistencyCheckReplyGenerator() => Arb.From( from frame in Generate() - from checksum in Generate() + from checksum in Generate() select new ConsistencyCheckReply { Frame = frame, @@ -250,6 +255,18 @@ from checksum in Generate() } ); + public static Arbitrary ConsistencyCheckFailGenerator() => Arb.From( + from frame in Generate() + from localChecksum in Generate() + from remoteChecksum in Generate() + select new ConsistencyCheckFail + { + Frame = frame, + LocalChecksum = localChecksum, + RemoteChecksum = remoteChecksum, + } + ); + public static Arbitrary InputMsgGenerator( Arbitrary connectStatusGenerator ) => @@ -285,7 +302,8 @@ public static Arbitrary UpdMsgGenerator( Arbitrary keepAliveArb, Arbitrary inputAckArb, Arbitrary consistencyCheckReqArb, - Arbitrary consistencyCheckReplyArb + Arbitrary consistencyCheckReplyArb, + Arbitrary consistencyCheckFailArb ) => headerArb.Generator .Where(h => h.Type is not MessageType.Unknown) @@ -339,6 +357,12 @@ Arbitrary consistencyCheckReplyArb Header = header, ConsistencyCheckReply = x, }), + MessageType.ConsistencyCheckFail => + consistencyCheckFailArb.Generator.Select(x => new ProtocolMessage + { + Header = header, + ConsistencyCheckFail = x, + }), MessageType.InputAck => inputAckArb.Generator.Select(x => new ProtocolMessage {