Skip to content

Commit

Permalink
Input listener and replay session (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasteles authored Mar 25, 2024
1 parent e150116 commit 516ceb6
Show file tree
Hide file tree
Showing 28 changed files with 452 additions and 60 deletions.
20 changes: 20 additions & 0 deletions .run/SpaceWar (P1) (save).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SpaceWar (P1) (save)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/samples/SpaceWar/bin/Debug/net8.0/SpaceWar.exe" />
<option name="PROGRAM_PARAMETERS" value="9000 2 --save-to replay.inputs local 127.0.0.1:9001 " />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/samples/SpaceWar/bin/Debug/net8.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/samples/SpaceWar/SpaceWar.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
20 changes: 20 additions & 0 deletions .run/SpaceWar (replay).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SpaceWar (replay)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/samples/SpaceWar/bin/Debug/net8.0/SpaceWar.exe" />
<option name="PROGRAM_PARAMETERS" value="9000 2 replay replay.inputs" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/samples/SpaceWar/bin/Debug/net8.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/samples/SpaceWar/SpaceWar.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
34 changes: 28 additions & 6 deletions samples/SpaceWar/GameSessionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net;
using Backdash;
using Backdash.Sync.Input;
using Backdash.Sync.Input.Confirmed;
using SpaceWar.Logic;

namespace SpaceWar;
Expand All @@ -11,16 +12,15 @@ public static IRollbackSession<PlayerInputs, GameState> ParseArgs(
string[] args, RollbackOptions options
)
{
if (args is not [{ } portArg, { } playerCountArg, .. { } endpoints]
if (args is not [{ } portArg, { } playerCountArg, .. { } lastArgs]
|| !int.TryParse(portArg, out var port)
|| !int.TryParse(playerCountArg, out var playerCount)
)
|| !int.TryParse(playerCountArg, out var playerCount))
throw new InvalidOperationException("Invalid port argument");

if (playerCount > Config.MaxShips)
throw new InvalidOperationException("Too many players");

if (endpoints is ["sync-test"])
if (lastArgs is ["sync-test"])
return RollbackNetcode.CreateSyncTestSession<PlayerInputs, GameState>(
options: options,
services: new()
Expand All @@ -29,12 +29,33 @@ public static IRollbackSession<PlayerInputs, GameState> ParseArgs(
}
);

if (endpoints is ["spectate", { } hostArg] && IPEndPoint.TryParse(hostArg, out var host))
if (lastArgs is ["spectate", { } hostArg] && IPEndPoint.TryParse(hostArg, out var host))
return RollbackNetcode.CreateSpectatorSession<PlayerInputs, GameState>(
port, host, playerCount, options
);

var players = endpoints.Select((x, i) => ParsePlayer(playerCount, i + 1, x)).ToArray();
if (lastArgs is ["replay", { } replayFile])
{
if (!File.Exists(replayFile))
throw new InvalidOperationException("Invalid replay file");

var inputs = SaveInputsToFileListener.GetInputs(playerCount, replayFile).ToArray();

return RollbackNetcode.CreateReplaySession<PlayerInputs, GameState>(
playerCount, inputs
);
}


// save confirmed inputs to file
IInputListener<PlayerInputs>? saveInputsListener = null;
if (lastArgs is ["--save-to", { } filename, .. var argsAfterSave])
{
saveInputsListener = new SaveInputsToFileListener(filename);
lastArgs = argsAfterSave;
}

var players = lastArgs.Select((x, i) => ParsePlayer(playerCount, i + 1, x)).ToArray();
var localPlayer = players.FirstOrDefault(x => x.IsLocal());

if (localPlayer is null)
Expand All @@ -43,6 +64,7 @@ public static IRollbackSession<PlayerInputs, GameState> ParseArgs(
var session = RollbackNetcode.CreateSession<PlayerInputs, GameState>(port, options, new()
{
// LogWriter = new FileLogWriter($"log_{localPlayer.Number}.log"),
InputListener = saveInputsListener,
});

session.AddPlayers(players);
Expand Down
51 changes: 51 additions & 0 deletions samples/SpaceWar/SaveInputsToFileListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Backdash.Data;
using Backdash.Sync.Input.Confirmed;
using SpaceWar.Logic;

namespace SpaceWar;

sealed class SaveInputsToFileListener(string filename) : IInputListener<PlayerInputs>
{
const int InputSize = sizeof(PlayerInputs);
readonly FileStream fileStream = File.Create(filename);
readonly byte[] inputBuffer = new byte[InputSize];

public void OnConfirmed(in Frame frame, in ConfirmedInputs<PlayerInputs> inputs)
{
for (var i = 0; i < inputs.Count; i++)
{
var input = (ushort)inputs.Inputs[i];
Array.Clear(inputBuffer);
if (!input.TryFormat(inputBuffer, out _))
throw new InvalidOperationException("unable to save input");

fileStream.Write(inputBuffer);
}

fileStream.Write("\n"u8);
}

public void Dispose() => fileStream.Dispose();

public static IEnumerable<ConfirmedInputs<PlayerInputs>> GetInputs(int players, string file)
{
using var replayStream = File.OpenRead(file);
var buffer = new byte[InputSize * players];
var inputsBuffer = new PlayerInputs[players];
var lineBreak = new byte[1];

while (replayStream.Read(buffer) > 0)
{
for (var i = 0; i < players; i++)
{
if (ushort.TryParse(buffer.AsSpan().Slice(i * InputSize, InputSize), out var value))
inputsBuffer[i] = (PlayerInputs)value;
}

yield return new ConfirmedInputs<PlayerInputs>(inputsBuffer.AsSpan()[..players]);

if (replayStream.Read(lineBreak) is 0 || lineBreak[0] != '\n')
throw new InvalidOperationException("invalid replay file content");
}
}
}
3 changes: 3 additions & 0 deletions samples/SpaceWar/SpaceWar.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<ItemGroup>
<None Remove="Icon.ico"/>
<None Remove="Icon.bmp"/>
<None Update="replay.inputs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico"/>
Expand Down
Binary file added samples/SpaceWar/replay.inputs
Binary file not shown.
7 changes: 7 additions & 0 deletions samples/SpaceWar/scripts/linux/start_2players_save.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
dotnet build -c Release "$(dirname "$0")/../.."
pushd "$(dirname "$0")/../../bin/Release/net8.0" || exit
rm ./*.log
dotnet SpaceWar.dll 9000 2 --save-to replay.inputs local 127.0.0.1:9001 &
dotnet SpaceWar.dll 9001 2 127.0.0.1:9000 local &
popd || exit
6 changes: 6 additions & 0 deletions samples/SpaceWar/scripts/linux/start_replay.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
dotnet build -c Release "$(dirname "$0")/../.."
pushd "$(dirname "$0")/../../bin/Release/net8.0" || exit
rm ./*.log
dotnet SpaceWar.dll 9000 2 replay replay.inputs &
popd || exit
6 changes: 6 additions & 0 deletions samples/SpaceWar/scripts/windows/start_2players_save.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dotnet build -c Release %~dp0\..\..
pushd %~dp0\..\..\bin\Release\net8.0
del *.log
start SpaceWar 9000 2 --save-to replay.inputs local 127.0.0.1:9001
start SpaceWar 9001 2 127.0.0.1:9000 local
popd
5 changes: 5 additions & 0 deletions samples/SpaceWar/scripts/windows/start_replay.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dotnet build -c Release %~dp0\..\..
pushd %~dp0\..\..\bin\Release\net8.0
del *.log
start SpaceWar 9000 2 replay replay.inputs
popd
3 changes: 3 additions & 0 deletions src/Backdash/Backends/BackendServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Backdash.Network.Protocol;
using Backdash.Serialization;
using Backdash.Sync.Input;
using Backdash.Sync.Input.Confirmed;
using Backdash.Sync.State;
using Backdash.Sync.State.Stores;

Expand All @@ -23,11 +24,13 @@ sealed class BackendServices<TInput, TGameState>
public IInputGenerator<TInput>? InputGenerator { get; }
public IRandomNumberGenerator Random { get; }
public IDelayStrategy DelayStrategy { get; }
public IInputListener<TInput>? InputListener { get; }

public BackendServices(RollbackOptions options, SessionServices<TInput, TGameState>? services)
{
ChecksumProvider = services?.ChecksumProvider ?? ChecksumProviderFactory.Create<TGameState>();
StateStore = services?.StateStore ?? StateStoreFactory.Create(services?.StateSerializer);
InputListener = services?.InputListener;
Random = new DefaultRandomNumberGenerator(services?.Random ?? System.Random.Shared);
DelayStrategy = DelayStrategyFactory.Create(Random, options.Protocol.DelayStrategy);
InputGenerator = services?.InputGenerator;
Expand Down
37 changes: 30 additions & 7 deletions src/Backdash/Backends/Peer2PeerBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Backdash.Network.Protocol.Comm;
using Backdash.Serialization;
using Backdash.Sync.Input;
using Backdash.Sync.Input.Spectator;
using Backdash.Sync.Input.Confirmed;
using Backdash.Sync.State;

namespace Backdash.Backends;
Expand All @@ -19,7 +19,7 @@ sealed class Peer2PeerBackend<TInput, TGameState> : IRollbackSession<TInput, TGa
{
readonly RollbackOptions options;
readonly IBinarySerializer<TInput> inputSerializer;
readonly IBinarySerializer<CombinedInputs<TInput>> inputGroupSerializer;
readonly IBinarySerializer<ConfirmedInputs<TInput>> inputGroupSerializer;
readonly Logger logger;
readonly IStateStore<TGameState> stateStore;
readonly IProtocolClient udp;
Expand All @@ -28,15 +28,18 @@ sealed class Peer2PeerBackend<TInput, TGameState> : IRollbackSession<TInput, TGa
readonly ConnectionsState localConnections;
readonly IBackgroundJobManager backgroundJobManager;
readonly ProtocolInputEventQueue<TInput> peerInputEventQueue;
readonly IProtocolInputEventPublisher<CombinedInputs<TInput>> peerCombinedInputsEventPublisher;
readonly IProtocolInputEventPublisher<ConfirmedInputs<TInput>> peerCombinedInputsEventPublisher;
readonly PeerConnectionFactory peerConnectionFactory;
readonly List<PeerConnection<CombinedInputs<TInput>>> spectators;
readonly List<PeerConnection<ConfirmedInputs<TInput>>> spectators;
readonly List<PeerConnection<TInput>?> endpoints;
readonly HashSet<PlayerHandle> addedPlayers = [];
readonly HashSet<PlayerHandle> addedSpectators = [];
readonly IInputListener<TInput>? inputListener;

bool isSynchronizing = true;
int nextRecommendedInterval;
Frame nextSpectatorFrame = Frame.Zero;
Frame nextListenerFrame = Frame.Zero;
IRollbackHandler<TGameState> callbacks;
SynchronizedInput<TInput>[] syncInputBuffer = [];
Task backgroundJobTask = Task.CompletedTask;
Expand All @@ -61,9 +64,11 @@ BackendServices<TInput, TGameState> services
stateStore = services.StateStore;
backgroundJobManager = services.JobManager;
logger = services.Logger;
inputListener = services.InputListener;

peerInputEventQueue = new ProtocolInputEventQueue<TInput>();
peerCombinedInputsEventPublisher = new ProtocolCombinedInputsEventPublisher<TInput>(peerInputEventQueue);
inputGroupSerializer = new CombinedInputsSerializer<TInput>(inputSerializer);
inputGroupSerializer = new ConfirmedInputsSerializer<TInput>(inputSerializer);
localConnections = new(Max.NumberOfPlayers);
spectators = [];
endpoints = [];
Expand Down Expand Up @@ -107,6 +112,7 @@ public void Dispose()
stateStore.Dispose();
logger.Dispose();
backgroundJobManager.Dispose();
inputListener?.Dispose();
}

public void Close()
Expand Down Expand Up @@ -431,19 +437,36 @@ void DoSync()
{
if (NumberOfSpectators > 0)
{
GameInput<CombinedInputs<TInput>> confirmed = new(nextSpectatorFrame);
GameInput<ConfirmedInputs<TInput>> confirmed = new(nextSpectatorFrame);
while (nextSpectatorFrame <= minConfirmedFrame)
{
logger.Write(LogLevel.Debug, $"pushing frame {nextSpectatorFrame} to spectators");
if (!synchronizer.GetConfirmedInputGroup(in nextSpectatorFrame, ref confirmed))
break;

logger.Write(LogLevel.Debug, $"pushing frame {nextSpectatorFrame} to spectators");
for (var s = 0; s < spectators.Count; s++)
if (spectators[s].IsRunning)
spectators[s].SendInput(in confirmed);

nextSpectatorFrame++;
}
}

if (inputListener is not null)
{
GameInput<ConfirmedInputs<TInput>> confirmed = new(nextListenerFrame);
while (nextListenerFrame <= minConfirmedFrame)
{
if (!synchronizer.GetConfirmedInputGroup(in nextListenerFrame, ref confirmed))
break;

logger.Write(LogLevel.Debug, $"pushing frame {nextListenerFrame} to listener");
inputListener.OnConfirmed(in confirmed.Frame, in confirmed.Data);

nextListenerFrame++;
}
}

logger.Write(LogLevel.Debug, $"setting confirmed frame in sync to {minConfirmedFrame}");
synchronizer.SetLastConfirmedFrame(minConfirmedFrame);
}
Expand Down
Loading

0 comments on commit 516ceb6

Please sign in to comment.