Implement deterministic simulation spine
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
namespace SideScrollerGame.Sim;
|
||||
|
||||
public static class SimulationAssemblyMarker
|
||||
{
|
||||
}
|
||||
15
src/SideScrollerGame.Sim/Definitions/GameDefinition.cs
Normal file
15
src/SideScrollerGame.Sim/Definitions/GameDefinition.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Definitions;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record GameDefinition
|
||||
{
|
||||
public GameDefinition(ImmutableArray<PlayerDefinition> players)
|
||||
{
|
||||
Players = players.IsDefault ? ImmutableArray<PlayerDefinition>.Empty : players;
|
||||
}
|
||||
|
||||
public ImmutableArray<PlayerDefinition> Players { get; init; }
|
||||
}
|
||||
18
src/SideScrollerGame.Sim/Definitions/PlayerDefinition.cs
Normal file
18
src/SideScrollerGame.Sim/Definitions/PlayerDefinition.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SideScrollerGame.Sim.Math;
|
||||
|
||||
namespace SideScrollerGame.Sim.Definitions;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record PlayerDefinition
|
||||
{
|
||||
public PlayerDefinition(PlayerId playerId, FixPointVector2 spawnPosition)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
SpawnPosition = spawnPosition;
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; init; }
|
||||
|
||||
public FixPointVector2 SpawnPosition { get; init; }
|
||||
}
|
||||
@@ -59,4 +59,4 @@ public readonly struct SFixPointQuaternionTransform
|
||||
public SFixPointQuaternion m_Orientation { get; }
|
||||
|
||||
public SFixPointVector3 m_Size { get; }
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
#define RANGE_CHECK
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -14,20 +12,24 @@ public static class FixPoint16Ext
|
||||
{
|
||||
public static FixPoint16 Average(this IEnumerable<FixPoint16> source)
|
||||
{
|
||||
return Average(source, f => f);
|
||||
return source.Average(f => f);
|
||||
}
|
||||
|
||||
public static FixPoint16 Average<TSource>(this IEnumerable<TSource> source, Func<TSource, FixPoint16> selector)
|
||||
{
|
||||
using IEnumerator<TSource> e = source.GetEnumerator();
|
||||
using var e = source.GetEnumerator();
|
||||
if (!e.MoveNext())
|
||||
return FixPoint16.Zero;
|
||||
|
||||
long sum = selector(e.Current).m_Value;
|
||||
int count = 1;
|
||||
var count = 1;
|
||||
while (e.MoveNext())
|
||||
{
|
||||
checked { sum += selector(e.Current).m_Value; }
|
||||
checked
|
||||
{
|
||||
sum += selector(e.Current).m_Value;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -36,15 +38,13 @@ public static class FixPoint16Ext
|
||||
|
||||
public static FixPoint16 Sum(this IEnumerable<FixPoint16> source)
|
||||
{
|
||||
using IEnumerator<FixPoint16> e = source.GetEnumerator();
|
||||
using var e = source.GetEnumerator();
|
||||
if (!e.MoveNext())
|
||||
return FixPoint16.Zero;
|
||||
|
||||
var sum = e.Current;
|
||||
while (e.MoveNext())
|
||||
{
|
||||
sum += e.Current;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
@@ -83,9 +83,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Integer to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
m_Value = value << c_Shift;
|
||||
}
|
||||
@@ -95,25 +93,17 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (divisor == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
long iResult;
|
||||
|
||||
if (((uint)dividend & 0x80000000U) == ((uint)divisor & 0x80000000U))
|
||||
{
|
||||
iResult = (((long)dividend << c_Shift) + (divisor / 2)) / divisor;
|
||||
}
|
||||
iResult = (((long)dividend << c_Shift) + divisor / 2) / divisor;
|
||||
else
|
||||
{
|
||||
iResult = (((long)dividend << c_Shift) - (divisor / 2)) / divisor;
|
||||
}
|
||||
iResult = (((long)dividend << c_Shift) - divisor / 2) / divisor;
|
||||
#if RANGE_CHECK
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Division result out of range: {iResult}");
|
||||
}
|
||||
#endif
|
||||
m_Value = (int)iResult;
|
||||
}
|
||||
@@ -125,7 +115,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
if (divisor == 0L)
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
#endif
|
||||
while (dividend > (long.MaxValue >> c_Shift) || dividend < (long.MinValue >> c_Shift))
|
||||
while (dividend > long.MaxValue >> c_Shift || dividend < long.MinValue >> c_Shift)
|
||||
{
|
||||
dividend >>= 1;
|
||||
divisor >>= 1;
|
||||
@@ -134,9 +124,9 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
long iResult;
|
||||
|
||||
if (((ulong)dividend & 0x8000000000000000UL) == ((ulong)divisor & 0x8000000000000000UL))
|
||||
iResult = ((dividend << c_Shift) + (divisor / 2)) / divisor;
|
||||
iResult = ((dividend << c_Shift) + divisor / 2) / divisor;
|
||||
else
|
||||
iResult = ((dividend << c_Shift) - (divisor / 2)) / divisor;
|
||||
iResult = ((dividend << c_Shift) - divisor / 2) / divisor;
|
||||
|
||||
#if RANGE_CHECK
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
@@ -149,27 +139,19 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Double to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
if (value < 0.0)
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) - 0.5);
|
||||
}
|
||||
m_Value = (int)(value * c_Multiplier - 0.5);
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) + 0.5);
|
||||
}
|
||||
m_Value = (int)(value * c_Multiplier + 0.5);
|
||||
}
|
||||
|
||||
public FixPoint16(float value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Single to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
m_Value = 0;
|
||||
Update(value);
|
||||
@@ -233,13 +215,10 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
case EParseState.TrimStartWhitespace:
|
||||
{
|
||||
if (char.IsWhiteSpace(character))
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = EParseState.ParseMantissa;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EParseState.ParseMantissa:
|
||||
@@ -322,13 +301,10 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
case EParseState.TrimEndWhitespace:
|
||||
{
|
||||
if (char.IsWhiteSpace(character))
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException($"Unexpected character '{character}' after the number.");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -346,27 +322,19 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
if (exponentValue > 0)
|
||||
{
|
||||
while (exponentValue-- > 0)
|
||||
{
|
||||
if (denominatorPower > 0)
|
||||
{
|
||||
denominatorPower -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
numerator *= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
denominatorPower -= exponentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
numerator = mantissaNegative ? -numerator : numerator;
|
||||
denominator = 1;
|
||||
for (int i = 0; i < denominatorPower; ++i)
|
||||
for (var i = 0; i < denominatorPower; ++i)
|
||||
denominator *= 10;
|
||||
}
|
||||
|
||||
@@ -439,9 +407,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
unchecked
|
||||
{
|
||||
if (m_Value < 0)
|
||||
{
|
||||
return -(-m_Value >> c_Shift);
|
||||
}
|
||||
|
||||
return m_Value >> c_Shift;
|
||||
}
|
||||
@@ -499,13 +465,13 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
return ToDouble() * 360.0;
|
||||
}
|
||||
|
||||
public override readonly string ToString()
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{ToDouble()}[0x{m_Value:x}]";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly int GetHashCode()
|
||||
public readonly override int GetHashCode()
|
||||
{
|
||||
return m_Value.GetHashCode();
|
||||
}
|
||||
@@ -517,12 +483,10 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly bool Equals(object? obj)
|
||||
public readonly override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != typeof(FixPoint16))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((FixPoint16)obj).m_Value == m_Value;
|
||||
}
|
||||
@@ -596,13 +560,11 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var iResult = (long)a.m_Value + b.m_Value;
|
||||
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Addition result out of range: {iResult}");
|
||||
}
|
||||
|
||||
return new() { m_Value = (int)iResult };
|
||||
#else
|
||||
return new FixPoint16 { m_Value = a.m_Value + b.m_Value };
|
||||
return new FixPoint16 { m_Value = a.m_Value + b.m_Value };
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -616,13 +578,11 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var iResult = (long)a.m_Value - b.m_Value;
|
||||
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Substraction result out of range: {iResult}");
|
||||
}
|
||||
|
||||
return new() { m_Value = (int)iResult };
|
||||
#else
|
||||
return new FixPoint16 { m_Value = a.m_Value - b.m_Value };
|
||||
return new FixPoint16 { m_Value = a.m_Value - b.m_Value };
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -634,9 +594,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (a.m_Value < 0 && -a.m_Value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Negation result out of range: {a.m_Value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = -a.m_Value };
|
||||
}
|
||||
@@ -647,12 +605,10 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var iResult = (((long)a.m_Value * b.m_Value) + c_Half) >> c_Shift;
|
||||
var iResult = ((long)a.m_Value * b.m_Value + c_Half) >> c_Shift;
|
||||
#if RANGE_CHECK
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Multiplication result out of range: {iResult}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (int)iResult };
|
||||
}
|
||||
@@ -667,13 +623,11 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var iResult = (long)a.m_Value * value;
|
||||
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Multiplication result out of range: {iResult}");
|
||||
}
|
||||
|
||||
return new() { m_Value = (int)iResult };
|
||||
#else
|
||||
return new FixPoint16 { m_Value = a.m_Value * value };
|
||||
return new FixPoint16 { m_Value = a.m_Value * value };
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -685,16 +639,12 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
if ((a.m_Value & 0x80000000) == (value & 0x80000000))
|
||||
{
|
||||
return new() { m_Value = (int)(((long)a.m_Value + (value / 2)) / value) };
|
||||
}
|
||||
return new() { m_Value = (int)(((long)a.m_Value + value / 2) / value) };
|
||||
|
||||
return new() { m_Value = (int)(((long)a.m_Value - (value / 2)) / value) };
|
||||
return new() { m_Value = (int)(((long)a.m_Value - value / 2) / value) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,25 +655,17 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (b.m_Value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
long iResult;
|
||||
|
||||
if ((a.m_Value & 0x80000000) == (b.m_Value & 0x80000000))
|
||||
{
|
||||
iResult = (((long)a.m_Value << c_Shift) + (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
iResult = (((long)a.m_Value << c_Shift) + b.m_Value / 2) / b.m_Value;
|
||||
else
|
||||
{
|
||||
iResult = (((long)a.m_Value << c_Shift) - (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
iResult = (((long)a.m_Value << c_Shift) - b.m_Value / 2) / b.m_Value;
|
||||
#if RANGE_CHECK
|
||||
if (iResult < c_LongMin || iResult > c_LongMax)
|
||||
{
|
||||
throw new ArithmeticException($"Division result out of range: {iResult}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (int)iResult };
|
||||
}
|
||||
@@ -865,9 +807,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
value = IntMath.Sqrt(value);
|
||||
#if RANGE_CHECK
|
||||
if (value > 0x7fffffff)
|
||||
{
|
||||
throw new ArithmeticException($"Length out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (int)value };
|
||||
}
|
||||
@@ -880,9 +820,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var value = aSquared + bSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
|
||||
return value;
|
||||
@@ -896,17 +834,13 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var value = aSquared + bSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
|
||||
value = IntMath.Sqrt(value);
|
||||
#if RANGE_CHECK
|
||||
if (value > 0x7fffffff)
|
||||
{
|
||||
throw new ArithmeticException($"Length out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (int)value };
|
||||
}
|
||||
@@ -919,24 +853,18 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var value = aSquared + bSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
value += cSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
|
||||
value = IntMath.Sqrt(value);
|
||||
#if RANGE_CHECK
|
||||
if (value > 0x7fffffff)
|
||||
{
|
||||
throw new ArithmeticException($"Length out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (int)value };
|
||||
}
|
||||
@@ -967,17 +895,13 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value.m_Value < MinusOne.m_Value)
|
||||
{
|
||||
throw new ArithmeticException($"Asin number out of range: {value.m_Value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = -s_AsinTable[-value.m_Value] };
|
||||
}
|
||||
#if RANGE_CHECK
|
||||
if (value.m_Value > One.m_Value)
|
||||
{
|
||||
throw new ArithmeticException($"Asin number out of range: {value.m_Value}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = s_AsinTable[value.m_Value] };
|
||||
}
|
||||
@@ -994,17 +918,13 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
if (value.m_Value < 0)
|
||||
{
|
||||
if (-value.m_Value < s_AtanTable.Length)
|
||||
{
|
||||
return new() { m_Value = s_AtanTable[-value.m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = MinusHalfPi.m_Value + s_AtanTable[-(One / value).m_Value] };
|
||||
}
|
||||
|
||||
if (value.m_Value < s_AtanTable.Length)
|
||||
{
|
||||
return new() { m_Value = -s_AtanTable[value.m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = s_AtanTable[(One / value).m_Value] };
|
||||
}
|
||||
@@ -1017,9 +937,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
if (y.m_Value == 0)
|
||||
{
|
||||
if (x.m_Value >= 0)
|
||||
{
|
||||
return Zero;
|
||||
}
|
||||
|
||||
return Pi;
|
||||
}
|
||||
@@ -1027,51 +945,39 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
if (y.m_Value > 0)
|
||||
{
|
||||
if (x.m_Value == 0)
|
||||
{
|
||||
return HalfPi;
|
||||
}
|
||||
|
||||
if (x.m_Value > 0)
|
||||
{
|
||||
// x > 0, y > 0
|
||||
if (y.m_Value <= x.m_Value)
|
||||
{
|
||||
return new() { m_Value = s_AtanTable[(y / x).m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = HalfPi.m_Value - s_AtanTable[(x / y).m_Value] };
|
||||
}
|
||||
|
||||
// x < 0, y > 0
|
||||
if (y.m_Value <= -x.m_Value)
|
||||
{
|
||||
return new() { m_Value = Pi.m_Value - s_AtanTable[-(y / x).m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = HalfPi.m_Value + s_AtanTable[-(x / y).m_Value] };
|
||||
}
|
||||
|
||||
if (x.m_Value == 0)
|
||||
{
|
||||
return MinusHalfPi;
|
||||
}
|
||||
|
||||
if (x.m_Value > 0)
|
||||
{
|
||||
// x > 0, y < 0
|
||||
if (-y.m_Value <= x.m_Value)
|
||||
{
|
||||
return new() { m_Value = -s_AtanTable[-(y / x).m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = MinusHalfPi.m_Value + s_AtanTable[-(x / y).m_Value] };
|
||||
}
|
||||
|
||||
// x < 0, y < 0
|
||||
if (y.m_Value >= x.m_Value)
|
||||
{
|
||||
return new() { m_Value = MinusPi.m_Value + s_AtanTable[(y / x).m_Value] };
|
||||
}
|
||||
|
||||
return new() { m_Value = MinusHalfPi.m_Value - s_AtanTable[(x / y).m_Value] };
|
||||
}
|
||||
@@ -1083,9 +989,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
var iResult = (b.m_Value - a.m_Value) & c_FractionMask;
|
||||
|
||||
if (iResult > Pi.m_Value)
|
||||
{
|
||||
iResult -= TwoPi.m_Value;
|
||||
}
|
||||
|
||||
return new() { m_Value = iResult };
|
||||
}
|
||||
@@ -1106,7 +1010,7 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16 Lerp(FixPoint16 value1, FixPoint16 value2, FixPoint16 amount)
|
||||
{
|
||||
return value1 + (amount * (value2 - value1));
|
||||
return value1 + amount * (value2 - value1);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -1175,13 +1079,9 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
public void Update(float value)
|
||||
{
|
||||
if (value < 0.0f)
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) - 0.5f);
|
||||
}
|
||||
m_Value = (int)(value * c_MultiplierFloat - 0.5f);
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) + 0.5f);
|
||||
}
|
||||
m_Value = (int)(value * c_MultiplierFloat + 0.5f);
|
||||
}
|
||||
|
||||
public int m_Value;
|
||||
@@ -1190,28 +1090,28 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new FixPoint16 { m_Value = m_Value * 2 };
|
||||
get => new() { m_Value = m_Value * 2 };
|
||||
}
|
||||
|
||||
public FixPoint16 Quadrupled
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new FixPoint16 { m_Value = m_Value * 4 };
|
||||
get => new() { m_Value = m_Value * 4 };
|
||||
}
|
||||
|
||||
public FixPoint16 Halved
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new FixPoint16 { m_Value = m_Value / 2 };
|
||||
get => new() { m_Value = m_Value / 2 };
|
||||
}
|
||||
|
||||
public FixPoint16 Quartered
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new FixPoint16 { m_Value = m_Value / 4 };
|
||||
get => new() { m_Value = m_Value / 4 };
|
||||
}
|
||||
|
||||
public static readonly FixPoint16 Zero = default;
|
||||
@@ -1234,4 +1134,4 @@ public partial struct FixPoint16 : IEquatable<FixPoint16>, IComparable<FixPoint1
|
||||
public static readonly FixPoint16 MinusHalfPi = -HalfPi;
|
||||
public static readonly FixPoint16 MinusQuaterPi = -QuaterPi;
|
||||
public static readonly FixPoint16 InvSqrt2 = One / Sqrt(2);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
#define RANGE_CHECK
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SideScrollerGame.Sim.Math;
|
||||
@@ -23,9 +22,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Long to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
m_Value = value << c_Shift;
|
||||
}
|
||||
@@ -34,36 +31,24 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Double to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
if (value < 0.0)
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) - 0.5);
|
||||
}
|
||||
m_Value = (int)(value * c_Multiplier - 0.5);
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) + 0.5);
|
||||
}
|
||||
m_Value = (int)(value * c_Multiplier + 0.5);
|
||||
}
|
||||
|
||||
public FixPoint16Long(float value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Single to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
if (value < 0.0f)
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) - 0.5f);
|
||||
}
|
||||
m_Value = (int)(value * c_MultiplierFloat - 0.5f);
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) + 0.5f);
|
||||
}
|
||||
m_Value = (int)(value * c_MultiplierFloat + 0.5f);
|
||||
}
|
||||
|
||||
public long ToLongFloor()
|
||||
@@ -79,9 +64,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
public long ToLongRound()
|
||||
{
|
||||
if (m_Value < 0)
|
||||
{
|
||||
return -((-m_Value + c_Half) >> c_Shift);
|
||||
}
|
||||
|
||||
return (m_Value + c_Half) >> c_Shift;
|
||||
}
|
||||
@@ -89,9 +72,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
public long ToLong()
|
||||
{
|
||||
if (m_Value < 0)
|
||||
{
|
||||
return -(-m_Value >> c_Shift);
|
||||
}
|
||||
|
||||
return m_Value >> c_Shift;
|
||||
}
|
||||
@@ -120,7 +101,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
if (obj is not FixPoint16Long other)
|
||||
return -1;
|
||||
|
||||
|
||||
return m_Value.CompareTo(other.m_Value);
|
||||
}
|
||||
|
||||
@@ -132,9 +113,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((FixPoint16Long)obj).m_Value == m_Value;
|
||||
}
|
||||
@@ -203,12 +182,10 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
Int128 bigA = a.m_Value;
|
||||
Int128 bigB = b.m_Value;
|
||||
var result = ((bigA * bigB) + c_Half) >> c_Shift;
|
||||
var result = (bigA * bigB + c_Half) >> c_Shift;
|
||||
#if RANGE_CHECK
|
||||
if (result < long.MinValue || result > long.MaxValue)
|
||||
{
|
||||
throw new ArithmeticException($"Multiplication result out of range: {result}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (long)result };
|
||||
}
|
||||
@@ -217,25 +194,17 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (b.m_Value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
Int128 result;
|
||||
|
||||
if (((ulong)a.m_Value & 0x8000000000000000UL) == ((ulong)b.m_Value & 0x8000000000000000UL))
|
||||
{
|
||||
result = (((Int128)a.m_Value << c_Shift) + (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
result = (((Int128)a.m_Value << c_Shift) + b.m_Value / 2) / b.m_Value;
|
||||
else
|
||||
{
|
||||
result = (((Int128)a.m_Value << c_Shift) - (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
result = (((Int128)a.m_Value << c_Shift) - b.m_Value / 2) / b.m_Value;
|
||||
#if RANGE_CHECK
|
||||
if (result < long.MinValue || result > long.MaxValue)
|
||||
{
|
||||
throw new ArithmeticException($"Division result out of range: {result}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (long)result };
|
||||
}
|
||||
@@ -254,16 +223,12 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
if (((a.m_Value >> 32) & 0x80000000) == (value & 0x80000000))
|
||||
{
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value + (value / 2)) / value) };
|
||||
}
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value + value / 2) / value) };
|
||||
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value - (value / 2)) / value) };
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value - value / 2) / value) };
|
||||
}
|
||||
|
||||
public static implicit operator FixPoint16Long(int value)
|
||||
@@ -354,9 +319,7 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
var value = aSquared + bSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
|
||||
return new() { m_Value = IntMath.Sqrt(value) };
|
||||
@@ -383,4 +346,4 @@ public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquata
|
||||
public static readonly FixPoint16Long MinusOne = new(-1);
|
||||
public static readonly FixPoint16Long Half = new() { m_Value = One.m_Value / 2 };
|
||||
public static readonly FixPoint16Long MinusHalf = -Half;
|
||||
}
|
||||
}
|
||||
9
src/SideScrollerGame.Sim/Input/InputButton.cs
Normal file
9
src/SideScrollerGame.Sim/Input/InputButton.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SideScrollerGame.Sim.Input;
|
||||
|
||||
public enum InputButton
|
||||
{
|
||||
Jump = 0,
|
||||
FirePrimary = 1,
|
||||
FireSecondary = 2,
|
||||
Dash = 3
|
||||
}
|
||||
18
src/SideScrollerGame.Sim/Input/SimulationAction.cs
Normal file
18
src/SideScrollerGame.Sim/Input/SimulationAction.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Input;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public abstract record SimulationAction;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record MoveAxisChanged(PlayerId PlayerId, sbyte X, sbyte Y) : SimulationAction;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record AimAxisChanged(PlayerId PlayerId, short X, short Y) : SimulationAction;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record ButtonChanged(PlayerId PlayerId, InputButton Button, bool IsPressed) : SimulationAction;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record WeaponSlotSelected(PlayerId PlayerId, int SlotIndex) : SimulationAction;
|
||||
23
src/SideScrollerGame.Sim/Input/TickActionBatch.cs
Normal file
23
src/SideScrollerGame.Sim/Input/TickActionBatch.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Input;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record TickActionBatch
|
||||
{
|
||||
public TickActionBatch(int tick, ImmutableArray<SimulationAction> actions)
|
||||
{
|
||||
Tick = tick;
|
||||
Actions = actions.IsDefault ? ImmutableArray<SimulationAction>.Empty : actions;
|
||||
}
|
||||
|
||||
public static TickActionBatch Empty(int tick)
|
||||
{
|
||||
return new(tick, ImmutableArray<SimulationAction>.Empty);
|
||||
}
|
||||
|
||||
public int Tick { get; init; }
|
||||
|
||||
public ImmutableArray<SimulationAction> Actions { get; init; }
|
||||
}
|
||||
3
src/SideScrollerGame.Sim/PlayerId.cs
Normal file
3
src/SideScrollerGame.Sim/PlayerId.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Sim;
|
||||
|
||||
public readonly record struct PlayerId(int Value);
|
||||
18
src/SideScrollerGame.Sim/Replay/RecordedTick.cs
Normal file
18
src/SideScrollerGame.Sim/Replay/RecordedTick.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SideScrollerGame.Sim.Input;
|
||||
|
||||
namespace SideScrollerGame.Sim.Replay;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record RecordedTick
|
||||
{
|
||||
public RecordedTick(TickActionBatch actionBatch, int expectedStateHash)
|
||||
{
|
||||
ActionBatch = actionBatch;
|
||||
ExpectedStateHash = expectedStateHash;
|
||||
}
|
||||
|
||||
public TickActionBatch ActionBatch { get; init; }
|
||||
|
||||
public int ExpectedStateHash { get; init; }
|
||||
}
|
||||
30
src/SideScrollerGame.Sim/Replay/ReplayPlayer.cs
Normal file
30
src/SideScrollerGame.Sim/Replay/ReplayPlayer.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Immutable;
|
||||
using SideScrollerGame.Sim.Definitions;
|
||||
using SideScrollerGame.Sim.Serialization;
|
||||
|
||||
namespace SideScrollerGame.Sim.Replay;
|
||||
|
||||
public static class ReplayPlayer
|
||||
{
|
||||
public static ImmutableArray<int> Play(ReplayRecord replay, GameDefinition gameDefinition, SimulationConfig config)
|
||||
{
|
||||
if (replay.ContentHash != GameDefinitionHasher.Compute(gameDefinition))
|
||||
throw new InvalidOperationException("Replay content hash does not match the supplied game definition.");
|
||||
|
||||
if (replay.TicksPerSecond != config.TicksPerSecond)
|
||||
throw new InvalidOperationException("Replay tick rate does not match the supplied simulation config.");
|
||||
|
||||
var hashes = ImmutableArray.CreateBuilder<int>(replay.Ticks.Length);
|
||||
Simulation simulation = new(gameDefinition, config, replay.Seed);
|
||||
foreach (var recordedTick in replay.Ticks)
|
||||
{
|
||||
var result = simulation.Step(recordedTick.ActionBatch);
|
||||
if (result.StateHash != recordedTick.ExpectedStateHash)
|
||||
throw new InvalidOperationException($"Replay diverged at tick {recordedTick.ActionBatch.Tick}.");
|
||||
|
||||
hashes.Add(result.StateHash);
|
||||
}
|
||||
|
||||
return hashes.MoveToImmutable();
|
||||
}
|
||||
}
|
||||
24
src/SideScrollerGame.Sim/Replay/ReplayRecord.cs
Normal file
24
src/SideScrollerGame.Sim/Replay/ReplayRecord.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Replay;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record ReplayRecord
|
||||
{
|
||||
public ReplayRecord(int contentHash, int seed, int ticksPerSecond, ImmutableArray<RecordedTick> ticks)
|
||||
{
|
||||
ContentHash = contentHash;
|
||||
Seed = seed;
|
||||
TicksPerSecond = ticksPerSecond;
|
||||
Ticks = ticks.IsDefault ? ImmutableArray<RecordedTick>.Empty : ticks;
|
||||
}
|
||||
|
||||
public int ContentHash { get; init; }
|
||||
|
||||
public int Seed { get; init; }
|
||||
|
||||
public int TicksPerSecond { get; init; }
|
||||
|
||||
public ImmutableArray<RecordedTick> Ticks { get; init; }
|
||||
}
|
||||
27
src/SideScrollerGame.Sim/Replay/ReplayRecorder.cs
Normal file
27
src/SideScrollerGame.Sim/Replay/ReplayRecorder.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Immutable;
|
||||
using SideScrollerGame.Sim.Definitions;
|
||||
using SideScrollerGame.Sim.Input;
|
||||
using SideScrollerGame.Sim.Runtime;
|
||||
using SideScrollerGame.Sim.Serialization;
|
||||
|
||||
namespace SideScrollerGame.Sim.Replay;
|
||||
|
||||
public sealed class ReplayRecorder
|
||||
{
|
||||
public ReplayRecorder()
|
||||
{
|
||||
m_Ticks = new();
|
||||
}
|
||||
|
||||
public void Append(TickActionBatch actionBatch, TickResult tickResult)
|
||||
{
|
||||
m_Ticks.Add(new(actionBatch, tickResult.StateHash));
|
||||
}
|
||||
|
||||
public ReplayRecord Build(GameDefinition gameDefinition, SimulationConfig config, int seed)
|
||||
{
|
||||
return new(GameDefinitionHasher.Compute(gameDefinition), seed, config.TicksPerSecond, m_Ticks.ToImmutableArray());
|
||||
}
|
||||
|
||||
private readonly List<RecordedTick> m_Ticks;
|
||||
}
|
||||
24
src/SideScrollerGame.Sim/Runtime/PlayerSnapshot.cs
Normal file
24
src/SideScrollerGame.Sim/Runtime/PlayerSnapshot.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SideScrollerGame.Sim.Math;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record PlayerSnapshot
|
||||
{
|
||||
public PlayerSnapshot(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Position = position;
|
||||
MoveAxisX = moveAxisX;
|
||||
MoveAxisY = moveAxisY;
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; init; }
|
||||
|
||||
public FixPointVector2 Position { get; init; }
|
||||
|
||||
public sbyte MoveAxisX { get; init; }
|
||||
|
||||
public sbyte MoveAxisY { get; init; }
|
||||
}
|
||||
68
src/SideScrollerGame.Sim/Runtime/PlayerState.cs
Normal file
68
src/SideScrollerGame.Sim/Runtime/PlayerState.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using SideScrollerGame.Sim.Input;
|
||||
using SideScrollerGame.Sim.Math;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
public sealed class PlayerState
|
||||
{
|
||||
public PlayerState(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY, short aimAxisX, short aimAxisY, int selectedWeaponSlot, int buttonMask)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Position = position;
|
||||
MoveAxisX = moveAxisX;
|
||||
MoveAxisY = moveAxisY;
|
||||
AimAxisX = aimAxisX;
|
||||
AimAxisY = aimAxisY;
|
||||
SelectedWeaponSlot = selectedWeaponSlot;
|
||||
ButtonMask = buttonMask;
|
||||
}
|
||||
|
||||
public PlayerState Clone()
|
||||
{
|
||||
return new(PlayerId, Position, MoveAxisX, MoveAxisY, AimAxisX, AimAxisY, SelectedWeaponSlot, ButtonMask);
|
||||
}
|
||||
|
||||
public void SetMoveAxis(sbyte x, sbyte y)
|
||||
{
|
||||
MoveAxisX = x;
|
||||
MoveAxisY = y;
|
||||
}
|
||||
|
||||
public void SetAimAxis(short x, short y)
|
||||
{
|
||||
AimAxisX = x;
|
||||
AimAxisY = y;
|
||||
}
|
||||
|
||||
public void SetButton(InputButton button, bool isPressed)
|
||||
{
|
||||
var mask = 1 << (int)button;
|
||||
ButtonMask = isPressed ? ButtonMask | mask : ButtonMask & ~mask;
|
||||
}
|
||||
|
||||
public void SelectWeaponSlot(int slotIndex)
|
||||
{
|
||||
SelectedWeaponSlot = slotIndex;
|
||||
}
|
||||
|
||||
public void Advance()
|
||||
{
|
||||
Position += new FixPointVector2(MoveAxisX, MoveAxisY);
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; }
|
||||
|
||||
public FixPointVector2 Position { get; private set; }
|
||||
|
||||
public sbyte MoveAxisX { get; private set; }
|
||||
|
||||
public sbyte MoveAxisY { get; private set; }
|
||||
|
||||
public short AimAxisX { get; private set; }
|
||||
|
||||
public short AimAxisY { get; private set; }
|
||||
|
||||
public int SelectedWeaponSlot { get; private set; }
|
||||
|
||||
public int ButtonMask { get; private set; }
|
||||
}
|
||||
20
src/SideScrollerGame.Sim/Runtime/SimulationEvent.cs
Normal file
20
src/SideScrollerGame.Sim/Runtime/SimulationEvent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record SimulationEvent
|
||||
{
|
||||
public SimulationEvent(string kind, int tick, PlayerId playerId)
|
||||
{
|
||||
Kind = kind;
|
||||
Tick = tick;
|
||||
PlayerId = playerId;
|
||||
}
|
||||
|
||||
public string Kind { get; init; }
|
||||
|
||||
public int Tick { get; init; }
|
||||
|
||||
public PlayerId PlayerId { get; init; }
|
||||
}
|
||||
52
src/SideScrollerGame.Sim/Runtime/SimulationState.cs
Normal file
52
src/SideScrollerGame.Sim/Runtime/SimulationState.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
public sealed class SimulationState
|
||||
{
|
||||
public SimulationState(int tick, int seed, ulong randomState, ulong lastRandomValue, ImmutableArray<PlayerState> players)
|
||||
{
|
||||
Tick = tick;
|
||||
Seed = seed;
|
||||
RandomState = randomState;
|
||||
LastRandomValue = lastRandomValue;
|
||||
Players = players.IsDefault ? ImmutableArray<PlayerState>.Empty : players;
|
||||
}
|
||||
|
||||
public SimulationState Clone()
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<PlayerState>(Players.Length);
|
||||
foreach (var player in Players)
|
||||
builder.Add(player.Clone());
|
||||
|
||||
return new(Tick, Seed, RandomState, LastRandomValue, builder.MoveToImmutable());
|
||||
}
|
||||
|
||||
public PlayerState GetRequiredPlayer(PlayerId playerId)
|
||||
{
|
||||
foreach (var player in Players)
|
||||
{
|
||||
if (player.PlayerId == playerId)
|
||||
return player;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unknown player id {playerId.Value}.");
|
||||
}
|
||||
|
||||
public void AdvanceTick(int tick, ulong randomState, ulong lastRandomValue)
|
||||
{
|
||||
Tick = tick;
|
||||
RandomState = randomState;
|
||||
LastRandomValue = lastRandomValue;
|
||||
}
|
||||
|
||||
public int Tick { get; private set; }
|
||||
|
||||
public int Seed { get; }
|
||||
|
||||
public ulong RandomState { get; private set; }
|
||||
|
||||
public ulong LastRandomValue { get; private set; }
|
||||
|
||||
public ImmutableArray<PlayerState> Players { get; }
|
||||
}
|
||||
24
src/SideScrollerGame.Sim/Runtime/TickResult.cs
Normal file
24
src/SideScrollerGame.Sim/Runtime/TickResult.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record TickResult
|
||||
{
|
||||
public TickResult(ImmutableArray<SimulationEvent> events, int stateHash, WorldSnapshot previousSnapshot, WorldSnapshot currentSnapshot)
|
||||
{
|
||||
Events = events.IsDefault ? ImmutableArray<SimulationEvent>.Empty : events;
|
||||
StateHash = stateHash;
|
||||
PreviousSnapshot = previousSnapshot;
|
||||
CurrentSnapshot = currentSnapshot;
|
||||
}
|
||||
|
||||
public ImmutableArray<SimulationEvent> Events { get; init; }
|
||||
|
||||
public int StateHash { get; init; }
|
||||
|
||||
public WorldSnapshot PreviousSnapshot { get; init; }
|
||||
|
||||
public WorldSnapshot CurrentSnapshot { get; init; }
|
||||
}
|
||||
24
src/SideScrollerGame.Sim/Runtime/WorldSnapshot.cs
Normal file
24
src/SideScrollerGame.Sim/Runtime/WorldSnapshot.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record WorldSnapshot
|
||||
{
|
||||
public WorldSnapshot(int tick, int stateHash, ulong lastRandomValue, ImmutableArray<PlayerSnapshot> players)
|
||||
{
|
||||
Tick = tick;
|
||||
StateHash = stateHash;
|
||||
LastRandomValue = lastRandomValue;
|
||||
Players = players.IsDefault ? ImmutableArray<PlayerSnapshot>.Empty : players;
|
||||
}
|
||||
|
||||
public int Tick { get; init; }
|
||||
|
||||
public int StateHash { get; init; }
|
||||
|
||||
public ulong LastRandomValue { get; init; }
|
||||
|
||||
public ImmutableArray<PlayerSnapshot> Players { get; init; }
|
||||
}
|
||||
25
src/SideScrollerGame.Sim/Serialization/DeterministicHash.cs
Normal file
25
src/SideScrollerGame.Sim/Serialization/DeterministicHash.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Serialization;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static class DeterministicHash
|
||||
{
|
||||
public static int Compute(byte[] data)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const uint offset = 2166136261;
|
||||
const uint prime = 16777619;
|
||||
|
||||
var hash = offset;
|
||||
foreach (var value in data)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using SideScrollerGame.Sim.Definitions;
|
||||
|
||||
namespace SideScrollerGame.Sim.Serialization;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static class GameDefinitionHasher
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record GameDefinitionDocument
|
||||
{
|
||||
public ImmutableArray<PlayerDefinitionDocument> Players { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record PlayerDefinitionDocument
|
||||
{
|
||||
public int PlayerId { get; init; }
|
||||
|
||||
public int SpawnX { get; init; }
|
||||
|
||||
public int SpawnY { get; init; }
|
||||
}
|
||||
|
||||
public static int Compute(GameDefinition gameDefinition)
|
||||
{
|
||||
List<PlayerDefinitionDocument> players = new(gameDefinition.Players.Length);
|
||||
foreach (var player in gameDefinition.Players)
|
||||
{
|
||||
players.Add(new()
|
||||
{
|
||||
PlayerId = player.PlayerId.Value,
|
||||
SpawnX = player.SpawnPosition.m_X.m_Value,
|
||||
SpawnY = player.SpawnPosition.m_Y.m_Value
|
||||
});
|
||||
}
|
||||
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(new GameDefinitionDocument { Players = players.ToImmutableArray() });
|
||||
|
||||
return DeterministicHash.Compute(bytes);
|
||||
}
|
||||
}
|
||||
146
src/SideScrollerGame.Sim/Serialization/ReplayRecordSerializer.cs
Normal file
146
src/SideScrollerGame.Sim/Serialization/ReplayRecordSerializer.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using SideScrollerGame.Sim.Input;
|
||||
using SideScrollerGame.Sim.Replay;
|
||||
|
||||
namespace SideScrollerGame.Sim.Serialization;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public static class ReplayRecordSerializer
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record ReplayRecordDocument
|
||||
{
|
||||
public int Version { get; init; }
|
||||
|
||||
public int ContentHash { get; init; }
|
||||
|
||||
public int Seed { get; init; }
|
||||
|
||||
public int TicksPerSecond { get; init; }
|
||||
|
||||
public ImmutableArray<RecordedTickDocument> Ticks { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record RecordedTickDocument
|
||||
{
|
||||
public int Tick { get; init; }
|
||||
|
||||
public int ExpectedStateHash { get; init; }
|
||||
|
||||
public ImmutableArray<SimulationActionDocument> Actions { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record SimulationActionDocument
|
||||
{
|
||||
public string Kind { get; init; } = string.Empty;
|
||||
|
||||
public int PlayerId { get; init; }
|
||||
|
||||
public int X { get; init; }
|
||||
|
||||
public int Y { get; init; }
|
||||
|
||||
public InputButton Button { get; init; }
|
||||
|
||||
public bool IsPressed { get; init; }
|
||||
|
||||
public int SlotIndex { get; init; }
|
||||
}
|
||||
|
||||
public static byte[] Serialize(ReplayRecord replayRecord)
|
||||
{
|
||||
List<RecordedTickDocument> ticks = new(replayRecord.Ticks.Length);
|
||||
foreach (var tick in replayRecord.Ticks)
|
||||
{
|
||||
List<SimulationActionDocument> actions = new(tick.ActionBatch.Actions.Length);
|
||||
foreach (var action in tick.ActionBatch.Actions)
|
||||
actions.Add(ToDocument(action));
|
||||
|
||||
ticks.Add(new()
|
||||
{
|
||||
Tick = tick.ActionBatch.Tick,
|
||||
ExpectedStateHash = tick.ExpectedStateHash,
|
||||
Actions = actions.ToImmutableArray()
|
||||
});
|
||||
}
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(new ReplayRecordDocument
|
||||
{
|
||||
Version = SimulationDefaults.ReplayFormatVersion,
|
||||
ContentHash = replayRecord.ContentHash,
|
||||
Seed = replayRecord.Seed,
|
||||
TicksPerSecond = replayRecord.TicksPerSecond,
|
||||
Ticks = ticks.ToImmutableArray()
|
||||
});
|
||||
}
|
||||
|
||||
public static ReplayRecord Deserialize(byte[] data)
|
||||
{
|
||||
var document = JsonSerializer.Deserialize<ReplayRecordDocument>(data) ?? throw new InvalidOperationException("Replay payload was empty.");
|
||||
if (document.Version != SimulationDefaults.ReplayFormatVersion)
|
||||
throw new NotSupportedException($"Unsupported replay version {document.Version}.");
|
||||
|
||||
var ticks = ImmutableArray.CreateBuilder<RecordedTick>(document.Ticks.Length);
|
||||
foreach (var tick in document.Ticks)
|
||||
{
|
||||
var actions = ImmutableArray.CreateBuilder<SimulationAction>(tick.Actions.Length);
|
||||
foreach (var action in tick.Actions)
|
||||
actions.Add(ToAction(action));
|
||||
|
||||
ticks.Add(new(new(tick.Tick, actions.MoveToImmutable()), tick.ExpectedStateHash));
|
||||
}
|
||||
|
||||
return new(document.ContentHash, document.Seed, document.TicksPerSecond, ticks.MoveToImmutable());
|
||||
}
|
||||
|
||||
private static SimulationActionDocument ToDocument(SimulationAction action)
|
||||
{
|
||||
return action switch
|
||||
{
|
||||
MoveAxisChanged moveAxisChanged => new()
|
||||
{
|
||||
Kind = nameof(MoveAxisChanged),
|
||||
PlayerId = moveAxisChanged.PlayerId.Value,
|
||||
X = moveAxisChanged.X,
|
||||
Y = moveAxisChanged.Y
|
||||
},
|
||||
AimAxisChanged aimAxisChanged => new()
|
||||
{
|
||||
Kind = nameof(AimAxisChanged),
|
||||
PlayerId = aimAxisChanged.PlayerId.Value,
|
||||
X = aimAxisChanged.X,
|
||||
Y = aimAxisChanged.Y
|
||||
},
|
||||
ButtonChanged buttonChanged => new()
|
||||
{
|
||||
Kind = nameof(ButtonChanged),
|
||||
PlayerId = buttonChanged.PlayerId.Value,
|
||||
Button = buttonChanged.Button,
|
||||
IsPressed = buttonChanged.IsPressed
|
||||
},
|
||||
WeaponSlotSelected weaponSlotSelected => new()
|
||||
{
|
||||
Kind = nameof(WeaponSlotSelected),
|
||||
PlayerId = weaponSlotSelected.PlayerId.Value,
|
||||
SlotIndex = weaponSlotSelected.SlotIndex
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported action type {action.GetType().Name}.")
|
||||
};
|
||||
}
|
||||
|
||||
private static SimulationAction ToAction(SimulationActionDocument action)
|
||||
{
|
||||
return action.Kind switch
|
||||
{
|
||||
nameof(MoveAxisChanged) => new MoveAxisChanged(new(action.PlayerId), checked((sbyte)action.X), checked((sbyte)action.Y)),
|
||||
nameof(AimAxisChanged) => new AimAxisChanged(new(action.PlayerId), checked((short)action.X), checked((short)action.Y)),
|
||||
nameof(ButtonChanged) => new ButtonChanged(new(action.PlayerId), action.Button, action.IsPressed),
|
||||
nameof(WeaponSlotSelected) => new WeaponSlotSelected(new(action.PlayerId), action.SlotIndex),
|
||||
_ => throw new NotSupportedException($"Unsupported action kind {action.Kind}.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using SideScrollerGame.Sim.Math;
|
||||
using SideScrollerGame.Sim.Runtime;
|
||||
|
||||
namespace SideScrollerGame.Sim.Serialization;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static class SimulationStateSerializer
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record SimulationStateDocument
|
||||
{
|
||||
public int Version { get; init; }
|
||||
|
||||
public int Tick { get; init; }
|
||||
|
||||
public int Seed { get; init; }
|
||||
|
||||
public ulong RandomState { get; init; }
|
||||
|
||||
public ulong LastRandomValue { get; init; }
|
||||
|
||||
public ImmutableArray<PlayerStateDocument> Players { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record PlayerStateDocument
|
||||
{
|
||||
public int PlayerId { get; init; }
|
||||
|
||||
public int PositionX { get; init; }
|
||||
|
||||
public int PositionY { get; init; }
|
||||
|
||||
public sbyte MoveAxisX { get; init; }
|
||||
|
||||
public sbyte MoveAxisY { get; init; }
|
||||
|
||||
public short AimAxisX { get; init; }
|
||||
|
||||
public short AimAxisY { get; init; }
|
||||
|
||||
public int SelectedWeaponSlot { get; init; }
|
||||
|
||||
public int ButtonMask { get; init; }
|
||||
}
|
||||
|
||||
public static byte[] Serialize(SimulationState state)
|
||||
{
|
||||
List<PlayerStateDocument> players = new(state.Players.Length);
|
||||
foreach (var player in state.Players)
|
||||
{
|
||||
players.Add(new()
|
||||
{
|
||||
PlayerId = player.PlayerId.Value,
|
||||
PositionX = player.Position.m_X.m_Value,
|
||||
PositionY = player.Position.m_Y.m_Value,
|
||||
MoveAxisX = player.MoveAxisX,
|
||||
MoveAxisY = player.MoveAxisY,
|
||||
AimAxisX = player.AimAxisX,
|
||||
AimAxisY = player.AimAxisY,
|
||||
SelectedWeaponSlot = player.SelectedWeaponSlot,
|
||||
ButtonMask = player.ButtonMask
|
||||
});
|
||||
}
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(new SimulationStateDocument
|
||||
{
|
||||
Version = SimulationDefaults.StateFormatVersion,
|
||||
Tick = state.Tick,
|
||||
Seed = state.Seed,
|
||||
RandomState = state.RandomState,
|
||||
LastRandomValue = state.LastRandomValue,
|
||||
Players = players.ToImmutableArray()
|
||||
});
|
||||
}
|
||||
|
||||
public static SimulationState Deserialize(byte[] data)
|
||||
{
|
||||
var document = JsonSerializer.Deserialize<SimulationStateDocument>(data) ?? throw new InvalidOperationException("Simulation state payload was empty.");
|
||||
if (document.Version != SimulationDefaults.StateFormatVersion)
|
||||
throw new NotSupportedException($"Unsupported simulation state version {document.Version}.");
|
||||
|
||||
var players = ImmutableArray.CreateBuilder<PlayerState>(document.Players.Length);
|
||||
foreach (var player in document.Players)
|
||||
{
|
||||
players.Add(new(new(player.PlayerId), new(new() { m_Value = player.PositionX }, new FixPoint16 { m_Value = player.PositionY }), player.MoveAxisX, player.MoveAxisY, player.AimAxisX, player.AimAxisY, player.SelectedWeaponSlot, player.ButtonMask));
|
||||
}
|
||||
|
||||
return new(document.Tick, document.Seed, document.RandomState, document.LastRandomValue, players.MoveToImmutable());
|
||||
}
|
||||
}
|
||||
202
src/SideScrollerGame.Sim/Simulation.cs
Normal file
202
src/SideScrollerGame.Sim/Simulation.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SideScrollerGame.Sim.Definitions;
|
||||
using SideScrollerGame.Sim.Input;
|
||||
using SideScrollerGame.Sim.Math;
|
||||
using SideScrollerGame.Sim.Runtime;
|
||||
using SideScrollerGame.Sim.Serialization;
|
||||
using SideScrollerGame.Sim.Verification;
|
||||
|
||||
namespace SideScrollerGame.Sim;
|
||||
|
||||
public sealed class Simulation
|
||||
{
|
||||
public Simulation(GameDefinition gameDefinition, SimulationConfig config, int seed) : this(gameDefinition, config, CreateInitialState(gameDefinition, seed), true)
|
||||
{
|
||||
}
|
||||
|
||||
private Simulation(GameDefinition gameDefinition, SimulationConfig config, SimulationState initialState, bool enableVerification)
|
||||
{
|
||||
m_GameDefinition = gameDefinition ?? throw new ArgumentNullException(nameof(gameDefinition));
|
||||
m_Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
m_EnableVerification = enableVerification;
|
||||
|
||||
ValidateDefinitions(gameDefinition);
|
||||
|
||||
CurrentState = initialState;
|
||||
var initialHash = ComputeStateHash(initialState);
|
||||
PreviousSnapshot = CreateSnapshot(initialState, initialHash);
|
||||
CurrentSnapshot = PreviousSnapshot;
|
||||
}
|
||||
|
||||
public TickResult Step(in TickActionBatch actions)
|
||||
{
|
||||
if (actions.Tick != CurrentTick + 1)
|
||||
throw new InvalidOperationException($"Expected tick {CurrentTick + 1} but received {actions.Tick}.");
|
||||
|
||||
TickResult? cloneResult = null;
|
||||
if (m_EnableVerification && m_Config.VerificationMode == VerificationMode.RoundTripAndStepClone)
|
||||
{
|
||||
var clone = LoadStateCore(SaveState(), m_GameDefinition, m_Config, false);
|
||||
cloneResult = clone.Step(actions);
|
||||
}
|
||||
|
||||
PreviousSnapshot = CurrentSnapshot;
|
||||
List<SimulationEvent> events = new();
|
||||
|
||||
ApplyActions(actions);
|
||||
var nextRandomState = AdvanceRandom();
|
||||
AdvancePlayers(actions.Tick, events);
|
||||
CurrentState.AdvanceTick(actions.Tick, nextRandomState, nextRandomState);
|
||||
|
||||
var stateHash = ComputeStateHash(CurrentState);
|
||||
CurrentSnapshot = CreateSnapshot(CurrentState, stateHash);
|
||||
|
||||
ValidateRoundTripState(stateHash);
|
||||
ValidateCloneResult(cloneResult, stateHash, actions.Tick);
|
||||
|
||||
return new(events.ToImmutableArray(), stateHash, PreviousSnapshot, CurrentSnapshot);
|
||||
}
|
||||
|
||||
public byte[] SaveState()
|
||||
{
|
||||
return SimulationStateSerializer.Serialize(CurrentState);
|
||||
}
|
||||
|
||||
public static Simulation LoadState(byte[] data, GameDefinition gameDefinition, SimulationConfig config)
|
||||
{
|
||||
return LoadStateCore(data, gameDefinition, config, true);
|
||||
}
|
||||
|
||||
private static Simulation LoadStateCore(byte[] data, GameDefinition gameDefinition, SimulationConfig config, bool enableVerification)
|
||||
{
|
||||
var state = SimulationStateSerializer.Deserialize(data);
|
||||
return new(gameDefinition, config, state, enableVerification);
|
||||
}
|
||||
|
||||
private static SimulationState CreateInitialState(GameDefinition gameDefinition, int seed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gameDefinition);
|
||||
ValidateDefinitions(gameDefinition);
|
||||
|
||||
var players = ImmutableArray.CreateBuilder<PlayerState>(gameDefinition.Players.Length);
|
||||
foreach (var player in gameDefinition.Players)
|
||||
players.Add(new(player.PlayerId, player.SpawnPosition, 0, 0, 0, 0, 0, 0));
|
||||
|
||||
var normalizedSeed = NormalizeSeed(seed);
|
||||
return new(0, seed, normalizedSeed, 0, players.MoveToImmutable());
|
||||
}
|
||||
|
||||
private static ulong NormalizeSeed(int seed)
|
||||
{
|
||||
return seed == 0 ? 1UL : unchecked((uint)seed);
|
||||
}
|
||||
|
||||
private static void ValidateDefinitions(GameDefinition gameDefinition)
|
||||
{
|
||||
HashSet<int> seen = new();
|
||||
foreach (var player in gameDefinition.Players)
|
||||
{
|
||||
if (!seen.Add(player.PlayerId.Value))
|
||||
throw new InvalidOperationException($"Duplicate player id {player.PlayerId.Value}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static int ComputeStateHash(SimulationState state)
|
||||
{
|
||||
return DeterministicHash.Compute(SimulationStateSerializer.Serialize(state));
|
||||
}
|
||||
|
||||
private static WorldSnapshot CreateSnapshot(SimulationState state, int stateHash)
|
||||
{
|
||||
var players = ImmutableArray.CreateBuilder<PlayerSnapshot>(state.Players.Length);
|
||||
foreach (var player in state.Players)
|
||||
players.Add(new(player.PlayerId, player.Position, player.MoveAxisX, player.MoveAxisY));
|
||||
|
||||
return new(state.Tick, stateHash, state.LastRandomValue, players.MoveToImmutable());
|
||||
}
|
||||
|
||||
private void ApplyActions(TickActionBatch actions)
|
||||
{
|
||||
foreach (var action in actions.Actions)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case MoveAxisChanged moveAxisChanged:
|
||||
CurrentState.GetRequiredPlayer(moveAxisChanged.PlayerId).SetMoveAxis(moveAxisChanged.X, moveAxisChanged.Y);
|
||||
break;
|
||||
case AimAxisChanged aimAxisChanged:
|
||||
CurrentState.GetRequiredPlayer(aimAxisChanged.PlayerId).SetAimAxis(aimAxisChanged.X, aimAxisChanged.Y);
|
||||
break;
|
||||
case ButtonChanged buttonChanged:
|
||||
CurrentState.GetRequiredPlayer(buttonChanged.PlayerId).SetButton(buttonChanged.Button, buttonChanged.IsPressed);
|
||||
break;
|
||||
case WeaponSlotSelected weaponSlotSelected:
|
||||
CurrentState.GetRequiredPlayer(weaponSlotSelected.PlayerId).SelectWeaponSlot(weaponSlotSelected.SlotIndex);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported action type {action.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AdvancePlayers(int tick, List<SimulationEvent> events)
|
||||
{
|
||||
foreach (var player in CurrentState.Players)
|
||||
{
|
||||
if (player.MoveAxisX != 0 || player.MoveAxisY != 0)
|
||||
{
|
||||
player.Advance();
|
||||
events.Add(new("PlayerMoved", tick, player.PlayerId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ulong AdvanceRandom()
|
||||
{
|
||||
SIntRandom random = new(CurrentState.RandomState);
|
||||
random.Next();
|
||||
return random.Seed;
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private void ValidateRoundTripState(int expectedHash)
|
||||
{
|
||||
if (!m_EnableVerification || m_Config.VerificationMode == VerificationMode.None)
|
||||
return;
|
||||
|
||||
var currentBytes = SaveState();
|
||||
var roundTrip = LoadStateCore(currentBytes, m_GameDefinition, m_Config, false);
|
||||
var roundTrippedBytes = roundTrip.SaveState();
|
||||
if (!currentBytes.AsSpan().SequenceEqual(roundTrippedBytes))
|
||||
throw new SimulationVerificationException("Round-trip serialization changed the saved state payload.");
|
||||
|
||||
var roundTripHash = ComputeStateHash(roundTrip.CurrentState);
|
||||
if (roundTripHash != expectedHash)
|
||||
throw new SimulationVerificationException("Round-trip serialization changed the state hash.");
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private static void ValidateCloneResult(TickResult? cloneResult, int stateHash, int tick)
|
||||
{
|
||||
if (cloneResult is null)
|
||||
return;
|
||||
|
||||
if (cloneResult.StateHash != stateHash)
|
||||
throw new SimulationVerificationException($"Clone replay diverged at tick {tick}.");
|
||||
}
|
||||
|
||||
public int CurrentTick => CurrentState.Tick;
|
||||
|
||||
public SimulationState CurrentState { get; }
|
||||
|
||||
public WorldSnapshot PreviousSnapshot { get; private set; }
|
||||
|
||||
public WorldSnapshot CurrentSnapshot { get; private set; }
|
||||
|
||||
private readonly SimulationConfig m_Config;
|
||||
|
||||
private readonly bool m_EnableVerification;
|
||||
|
||||
private readonly GameDefinition m_GameDefinition;
|
||||
}
|
||||
23
src/SideScrollerGame.Sim/SimulationConfig.cs
Normal file
23
src/SideScrollerGame.Sim/SimulationConfig.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SideScrollerGame.Sim.Verification;
|
||||
|
||||
namespace SideScrollerGame.Sim;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record SimulationConfig
|
||||
{
|
||||
public SimulationConfig(int ticksPerSecond, VerificationMode verificationMode)
|
||||
{
|
||||
if (ticksPerSecond <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(ticksPerSecond));
|
||||
|
||||
TicksPerSecond = ticksPerSecond;
|
||||
VerificationMode = verificationMode;
|
||||
}
|
||||
|
||||
public int TicksPerSecond { get; init; }
|
||||
|
||||
public VerificationMode VerificationMode { get; init; }
|
||||
|
||||
public static SimulationConfig Default { get; } = new(SimulationDefaults.DefaultTicksPerSecond, VerificationMode.None);
|
||||
}
|
||||
11
src/SideScrollerGame.Sim/SimulationDefaults.cs
Normal file
11
src/SideScrollerGame.Sim/SimulationDefaults.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public static class SimulationDefaults
|
||||
{
|
||||
public const int DefaultTicksPerSecond = 60;
|
||||
public const int ReplayFormatVersion = 1;
|
||||
public const int StateFormatVersion = 1;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Verification;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class SimulationVerificationException : Exception
|
||||
{
|
||||
public SimulationVerificationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SideScrollerGame.Sim.Verification;
|
||||
|
||||
public enum VerificationMode
|
||||
{
|
||||
None,
|
||||
RoundTripState,
|
||||
RoundTripAndStepClone
|
||||
}
|
||||
Reference in New Issue
Block a user