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
{