From b321e0c18dd7e3f57ca2661006ce6f6f93be03d4 Mon Sep 17 00:00:00 2001 From: Jamie Meyer <45072324+HeatXD@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:50:33 +0000 Subject: [PATCH 1/5] Replay Progress --- .../backends/utility/BroadcastStream.cs | 10 ++- src/session/backends/utility/ReplayFile.cs | 64 +++++++++++++++---- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/session/backends/utility/BroadcastStream.cs b/src/session/backends/utility/BroadcastStream.cs index b18d597..cfa515a 100644 --- a/src/session/backends/utility/BroadcastStream.cs +++ b/src/session/backends/utility/BroadcastStream.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; namespace PleaseResync.session.backends.utility { @@ -9,6 +10,8 @@ public class BroadcastStream private int _currentFrame, _availableFrame; private readonly List _frameBuffer; + public ReplayFile Replay; + public BroadcastStream(int initialBuffer = 30, uint inputSize = 1) { InputSize = inputSize; @@ -59,9 +62,12 @@ public bool GetFrameInput(out int frame, out byte[] input) return true; } - public void SaveToFile() + public string SaveToFile() { - ReplayFile.SaveToFile(InputSize, _availableFrame, [], _frameBuffer); + Replay.Init(InputSize, [], []); + Replay.SetData(_availableFrame, _frameBuffer); + var path = Replay.Save(); + return path; } } } diff --git a/src/session/backends/utility/ReplayFile.cs b/src/session/backends/utility/ReplayFile.cs index b8a73f8..83637e1 100644 --- a/src/session/backends/utility/ReplayFile.cs +++ b/src/session/backends/utility/ReplayFile.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading.Tasks; using MessagePack; namespace PleaseResync.session.backends.utility @@ -10,27 +12,63 @@ public class ReplayFile { [Key(0)] public uint InputSize; + [Key(1)] public int NumFrames; + [Key(2)] public List InitialState; + [Key(3)] public List InputFrames; - public static void SaveToFile(uint inpSize, int numFrames, List initState, List inpFrames) + [Key(4)] + public List MetaData; + + public void Init(uint inputSize, List initialState, List metaData) + { + InputSize = inputSize; + InitialState = new(initialState); + MetaData = new(metaData); + } + + public void SetData(int numFrames, List inputFrames) + { + NumFrames = numFrames; + InputFrames = new(inputFrames); + } + + public string Save(string folderPath = null) + { + if (string.IsNullOrWhiteSpace(folderPath)) + folderPath = "PRReplays"; + + Directory.CreateDirectory(folderPath); + + var rawData = MessagePackSerializer.Serialize(this); + + var compressed = Platform.RLEEncode(rawData.ToList()); + + string fileName = $"{Guid.NewGuid():N}.PRReplay"; + string fullPath = Path.Combine(folderPath, fileName); + + File.WriteAllBytes(fullPath, compressed.ToArray()); + return fullPath; + } + + public void LoadFromFile(string filePath) { - var file = new ReplayFile - { - InputSize = inpSize, - NumFrames = numFrames, - InitialState = Platform.RLEEncode(initState), - InputFrames = Platform.RLEEncode(inpFrames) - }; - - var fileData = MessagePackSerializer.Serialize(file); - string fileName = $"{Guid.NewGuid().ToString("N")}.PRReplay"; - - File.WriteAllBytesAsync(fileName, fileData); + if (!File.Exists(filePath)) + throw new FileNotFoundException("Replay file not found.", filePath); + + var compressed = File.ReadAllBytes(filePath); + + var rawData = Platform.RLEDecode(compressed.ToList()); + + var file = MessagePackSerializer.Deserialize(rawData.ToArray()); + + Init(file.InputSize, file.InitialState, file.MetaData); + SetData(file.NumFrames, file.InputFrames); } } } From cbb31897e8042f5468b1bb26043cc6a86eb88eb8 Mon Sep 17 00:00:00 2001 From: Jamie Meyer Date: Wed, 4 Jun 2025 23:34:37 +0200 Subject: [PATCH 2/5] Alot of progress almost done.. --- src/Session/Backends/ReplaySession.cs | 220 ++++++++++++++++++ src/input/GameInput.cs | 2 +- src/input/InputQueue.cs | 7 +- src/session/Device.cs | 2 +- src/session/DeviceMessage.cs | 2 +- src/session/Session.cs | 13 +- src/session/SessionAction.cs | 4 +- src/session/SessionAdapter.cs | 2 +- src/session/SessionEvent.cs | 2 +- .../adapters/LiteNetLibSessionAdapter.cs | 2 +- src/session/backends/Peer2PeerSession.cs | 7 +- src/session/backends/SpectatorSession.cs | 13 +- src/session/backends/SyncTestSession.cs | 1 + .../backends/utility/BroadcastStream.cs | 64 +++-- src/session/backends/utility/ReplayFile.cs | 15 +- src/synchronization/StateStorage.cs | 2 +- src/synchronization/Sync.cs | 28 ++- src/synchronization/TimeSync.cs | 4 +- 18 files changed, 328 insertions(+), 62 deletions(-) create mode 100644 src/Session/Backends/ReplaySession.cs diff --git a/src/Session/Backends/ReplaySession.cs b/src/Session/Backends/ReplaySession.cs new file mode 100644 index 0000000..667a3b0 --- /dev/null +++ b/src/Session/Backends/ReplaySession.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using PleaseResync.Session.Backends.Utility; +using PleaseResync.Synchronization; + +namespace PleaseResync.Session.Backends +{ + public class ReplaySession : Session + { + public enum ReplayState + { + NoFile, + FileLoaded, + Restarting, + Running, + Paused, + Stepping, + Jumping, + } + + private ReplayState _state; + private int _currentFrame = 0; + private int _requestedFrame = -1; + private readonly StateStorage _stateStorage; + private readonly BroadcastStream _broadcastStream; + + public ReplaySession() : base(0, 0, 0, false, true) + { + _state = ReplayState.NoFile; + _broadcastStream = new BroadcastStream(); + _stateStorage = new StateStorage(0); + } + + public void LoadFile(string filepath) + { + _broadcastStream.LoadReplayFile(filepath); + + _state = ReplayState.FileLoaded; + + var initialState = _broadcastStream.GetInitialState(); + _stateStorage.SaveFrame(0, initialState); + } + + public void Restart() + { + if (_state == ReplayState.NoFile) return; + + _state = ReplayState.Restarting; + } + + public void Pause() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Paused; + } + + public void Resume() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Running; + } + + public void Step() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Stepping; + _requestedFrame = _currentFrame + 1; + } + + public void GoToFrame(int frame) + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Jumping; + _requestedFrame = frame; + } + + + protected internal override Device LocalDevice => throw new System.NotImplementedException(); + + protected internal override Device[] AllDevices => throw new System.NotImplementedException(); + + public override void AddRemoteDevice(uint deviceId, uint playerCount, object remoteConfiguration) + { + throw new System.NotImplementedException(); + } + + public override void AddSpectatorDevice(object remoteConfiguration) + { + throw new System.NotImplementedException(); + } + + public override List AdvanceFrame(byte[] localInput = null) + { + var actions = new List(); + + switch (_state) + { + case ReplayState.NoFile: + break; + case ReplayState.FileLoaded: + case ReplayState.Restarting: + _currentFrame = 0; + _broadcastStream.SetCurrentFrame(0); + Resume(); + break; + case ReplayState.Running: + if (_broadcastStream.GetFrameInput(out var frame, out var input)) + { + if (_currentFrame == 0) + { + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + } + _currentFrame = frame; + actions.Add(new SessionAdvanceFrameAction(frame, input)); + } + break; + case ReplayState.Stepping: + if (_currentFrame + 1 == _requestedFrame && + _broadcastStream.GetFrameInput(out var stepFrame, out var stepInput)) + { + _currentFrame = stepFrame; + actions.Add(new SessionAdvanceFrameAction(stepFrame, stepInput)); + } + break; + case ReplayState.Jumping: + if (_currentFrame > _requestedFrame) + { + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + _currentFrame = 0; + } + _broadcastStream.SetCurrentFrame(_currentFrame); + for (int i = _currentFrame; i <= _requestedFrame; i++) + { + if (_broadcastStream.GetFrameInput(out var jumpFrame, out var jumpInput)) + { + _currentFrame = jumpFrame; + actions.Add(new SessionAdvanceFrameAction(jumpFrame, jumpInput)); + } + } + Resume(); + break; + case ReplayState.Paused: + break; + default: + break; + } + + return actions; + } + + public override uint AverageRollbackFrames() + { + throw new System.NotImplementedException(); + } + + public override int Frame() + { + return _currentFrame; + } + + public override int FrameAdvantage() + { + throw new System.NotImplementedException(); + } + + public override int FrameAdvantageDifference() + { + throw new System.NotImplementedException(); + } + + public override bool IsRunning() + { + return _state != ReplayState.NoFile; + } + + public override void Poll() + { + } + + public override int RemoteFrame() + { + throw new System.NotImplementedException(); + } + + public override int RemoteFrameAdvantage() + { + throw new System.NotImplementedException(); + } + + public override uint RollbackFrames() + { + throw new System.NotImplementedException(); + } + + public override void SetLocalDevice(uint deviceId, uint playerCount, uint frameDelay) + { + throw new System.NotImplementedException(); + } + + public override int State() + { + throw new System.NotImplementedException(); + } + + protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessage message) + { + throw new System.NotImplementedException(); + } + + protected internal override uint SendMessageTo(uint deviceId, DeviceMessage message) + { + throw new System.NotImplementedException(); + } + + public override void SaveToReplayFile() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/input/GameInput.cs b/src/input/GameInput.cs index e2d0ec9..63aa122 100644 --- a/src/input/GameInput.cs +++ b/src/input/GameInput.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Diagnostics; -namespace PleaseResync.input +namespace PleaseResync.Input { internal class GameInput { diff --git a/src/input/InputQueue.cs b/src/input/InputQueue.cs index 7c70b31..897ea80 100644 --- a/src/input/InputQueue.cs +++ b/src/input/InputQueue.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; -namespace PleaseResync.input +namespace PleaseResync.Input { internal class InputQueue { @@ -73,4 +72,4 @@ public void ResetPrediction(int frame) private int PreviousFrame(int offset) => offset == 0 ? QueueSize - 1 : offset - 1; } -} \ No newline at end of file +} diff --git a/src/session/Device.cs b/src/session/Device.cs index 5030f6a..977c386 100644 --- a/src/session/Device.cs +++ b/src/session/Device.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System; -namespace PleaseResync.session +namespace PleaseResync.Session { public class Device { diff --git a/src/session/DeviceMessage.cs b/src/session/DeviceMessage.cs index 272e398..17c43dd 100644 --- a/src/session/DeviceMessage.cs +++ b/src/session/DeviceMessage.cs @@ -1,6 +1,6 @@ using MessagePack; -namespace PleaseResync.session +namespace PleaseResync.Session { [Union(0, typeof(DeviceSyncMessage))] [Union(1, typeof(DeviceSyncConfirmMessage))] diff --git a/src/session/Session.cs b/src/session/Session.cs index 5b24450..ed6b0a1 100644 --- a/src/session/Session.cs +++ b/src/session/Session.cs @@ -1,9 +1,7 @@ using System.Diagnostics; using System.Collections.Generic; -using System.Net; -using System; -namespace PleaseResync.session +namespace PleaseResync.Session { /// /// Session is responsible for managing a pool of devices wanting to play your game together. @@ -59,8 +57,11 @@ public abstract class Session /// The size in bits of the input for one player. /// The number of devices taking part in this session. /// The total number of players accross all devices taking part in this session. - public Session(uint inputSize, uint deviceCount, uint totalPlayerCount, bool offline) + public Session(uint inputSize, uint deviceCount, uint totalPlayerCount, bool offline, bool replay = false) { + // we dont care about any of this when we are replaying + if (replay) return; + Debug.Assert(inputSize > 0); Debug.Assert(inputSize <= LIMIT_INPUT_SIZE); Debug.Assert(deviceCount >= 1); @@ -118,5 +119,7 @@ public Session(uint inputSize, uint deviceCount, uint totalPlayerCount, bool off public abstract uint RollbackFrames(); public abstract uint AverageRollbackFrames(); public abstract int State(); + + public abstract void SaveToReplayFile(); } -} \ No newline at end of file +} diff --git a/src/session/SessionAction.cs b/src/session/SessionAction.cs index 7becc28..2299ca0 100644 --- a/src/session/SessionAction.cs +++ b/src/session/SessionAction.cs @@ -1,7 +1,7 @@ using System.Diagnostics; -using PleaseResync.synchronization; +using PleaseResync.Synchronization; -namespace PleaseResync.session +namespace PleaseResync.Session { /// /// SessionAction is an action you must fulfill to give a chance to the Session to synchronize with other sessions. diff --git a/src/session/SessionAdapter.cs b/src/session/SessionAdapter.cs index 13a0121..c996211 100644 --- a/src/session/SessionAdapter.cs +++ b/src/session/SessionAdapter.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace PleaseResync.session +namespace PleaseResync.Session { /// /// SessionAdapter is the interface used to implement a way for the Session to communicate with remote devices. diff --git a/src/session/SessionEvent.cs b/src/session/SessionEvent.cs index b425a17..dd6b0e2 100644 --- a/src/session/SessionEvent.cs +++ b/src/session/SessionEvent.cs @@ -1,4 +1,4 @@ -namespace PleaseResync.session +namespace PleaseResync.Session { public abstract class SessionEvent { diff --git a/src/session/adapters/LiteNetLibSessionAdapter.cs b/src/session/adapters/LiteNetLibSessionAdapter.cs index ea38201..3b8781f 100644 --- a/src/session/adapters/LiteNetLibSessionAdapter.cs +++ b/src/session/adapters/LiteNetLibSessionAdapter.cs @@ -6,7 +6,7 @@ using System.Linq; using MessagePack; -namespace PleaseResync.session.adapters +namespace PleaseResync.Session.Adapters { public class LiteNetLibSessionAdapter : SessionAdapter, INetEventListener { diff --git a/src/session/backends/Peer2PeerSession.cs b/src/session/backends/Peer2PeerSession.cs index 110f251..21a910d 100644 --- a/src/session/backends/Peer2PeerSession.cs +++ b/src/session/backends/Peer2PeerSession.cs @@ -2,8 +2,9 @@ using System.Diagnostics; using System.Collections.Generic; using PleaseResync.synchronization; +using PleaseResync.Session.Backends.Utility; -namespace PleaseResync.session.backends +namespace PleaseResync.Session.Backends { /// /// Peer2PeerSession implements a session for devices wanting to play your game together via network. @@ -126,6 +127,8 @@ public override List AdvanceFrame(byte[] localInput) Debug.Assert(IsRunning(), "Session must be running before calling AdvanceFrame"); Debug.Assert(localInput != null); + if (Frame() == 1000) SaveToReplayFile(); + Poll(); return _sync.AdvanceSync(_localDevice.Id, localInput); } @@ -168,5 +171,7 @@ internal protected override void AddRemoteInput(uint deviceId, DeviceInputMessag public override uint RollbackFrames() => _sync.RollbackFrames(); public override uint AverageRollbackFrames() => _sync.AverageRollbackFrames(); public override int State() => (int)_sync.State(); + + public override void SaveToReplayFile() => _sync.SaveToReplayFile(); } } diff --git a/src/session/backends/SpectatorSession.cs b/src/session/backends/SpectatorSession.cs index 6b8658a..b969129 100644 --- a/src/session/backends/SpectatorSession.cs +++ b/src/session/backends/SpectatorSession.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using PleaseResync.session.backends.utility; +using PleaseResync.Session.Backends.Utility; -namespace PleaseResync.session.backends +namespace PleaseResync.Session.Backends { public class SpectatorSession : Session { @@ -46,11 +46,6 @@ public override List AdvanceFrame(byte[] localInput = null) { _currentFrame = frame; actions.Add(new SessionAdvanceFrameAction(frame, input)); - - if (frame == 1000|| frame == 5000 || frame == 10000) - { - _broadcastStream.SaveToFile(); - } } return actions; } @@ -130,7 +125,7 @@ protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessag { if (deviceId != _broadcastDevice.Id) return; // discard messages from other devices var count = message.EndFrame - message.StartFrame + 1; - var inpSize = (int)_broadcastStream.InputSize; + var inpSize = (int)_broadcastStream.InputSize(); for (var i = 0; i < count; i++) { @@ -150,5 +145,7 @@ public override void AddSpectatorDevice(object remoteConfiguration) { throw new NotImplementedException(); } + + public override void SaveToReplayFile() => _broadcastStream.SaveReplayFile(); } } diff --git a/src/session/backends/SyncTestSession.cs b/src/session/backends/SyncTestSession.cs index e69de29..bc1cebe 100644 --- a/src/session/backends/SyncTestSession.cs +++ b/src/session/backends/SyncTestSession.cs @@ -0,0 +1 @@ +// Todo In the future. diff --git a/src/session/backends/utility/BroadcastStream.cs b/src/session/backends/utility/BroadcastStream.cs index cfa515a..4c0a218 100644 --- a/src/session/backends/utility/BroadcastStream.cs +++ b/src/session/backends/utility/BroadcastStream.cs @@ -1,20 +1,24 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +using System; +using System.Collections.Generic; -namespace PleaseResync.session.backends.utility +namespace PleaseResync.Session.Backends.Utility { public class BroadcastStream { - public readonly uint InputSize; + private uint _inputSize; private readonly int _initialFrameBuffer; private int _currentFrame, _availableFrame; - private readonly List _frameBuffer; + private List _frameBuffer; - public ReplayFile Replay; + private ReplayFile _replay; + private List _initialState; public BroadcastStream(int initialBuffer = 30, uint inputSize = 1) { - InputSize = inputSize; + _initialState = []; + _replay = new ReplayFile(); + + _inputSize = inputSize; _initialFrameBuffer = initialBuffer; _currentFrame = 0; _availableFrame = -1; @@ -26,7 +30,7 @@ public void AddFrameInput(int frame, byte[] input) if (frame != _availableFrame + 1) return; // Append input to flat buffer - for (int i = 0; i < InputSize; i++) + for (var i = 0; i < _inputSize; i++) { _frameBuffer.Add(input[i]); } @@ -50,10 +54,10 @@ public bool GetFrameInput(out int frame, out byte[] input) } frame = _currentFrame; - input = new byte[InputSize]; + input = new byte[_inputSize]; - int startIndex = (int)(_currentFrame * InputSize); - for (int i = 0; i < InputSize; i++) + var startIndex = (int)(_currentFrame * _inputSize); + for (var i = 0; i < _inputSize; i++) { input[i] = _frameBuffer[startIndex + i]; } @@ -62,12 +66,42 @@ public bool GetFrameInput(out int frame, out byte[] input) return true; } - public string SaveToFile() + public string SaveReplayFile() { - Replay.Init(InputSize, [], []); - Replay.SetData(_availableFrame, _frameBuffer); - var path = Replay.Save(); + _replay.Init(_inputSize, _initialState); + _replay.SetData(_availableFrame, _frameBuffer); + var path = _replay.Save(); return path; } + + public void LoadReplayFile(string filepath) + { + _replay.LoadFromFile(filepath); + + _inputSize = _replay.InputSize; + + _currentFrame = 0; + _availableFrame = _replay.NumFrames; + + _frameBuffer.InsertRange(0, _replay.InputFrames); + + _initialState.Clear(); + _initialState.AddRange(_replay.InitialState); + } + + public uint InputSize() => _inputSize; + + public byte[] GetInitialState() => _initialState.ToArray(); + + public void SetInitialState(byte[] state) + { + _initialState.Clear(); + _initialState.AddRange(state); + } + + public void SetCurrentFrame(int frame) + { + _currentFrame = Math.Clamp(frame, 0, _availableFrame); + } } } diff --git a/src/session/backends/utility/ReplayFile.cs b/src/session/backends/utility/ReplayFile.cs index 83637e1..01d37c6 100644 --- a/src/session/backends/utility/ReplayFile.cs +++ b/src/session/backends/utility/ReplayFile.cs @@ -2,10 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using MessagePack; -namespace PleaseResync.session.backends.utility +namespace PleaseResync.Session.Backends.Utility { [MessagePackObject] public class ReplayFile @@ -22,14 +21,10 @@ public class ReplayFile [Key(3)] public List InputFrames; - [Key(4)] - public List MetaData; - - public void Init(uint inputSize, List initialState, List metaData) + public void Init(uint inputSize, List initialState) { InputSize = inputSize; InitialState = new(initialState); - MetaData = new(metaData); } public void SetData(int numFrames, List inputFrames) @@ -49,8 +44,8 @@ public string Save(string folderPath = null) var compressed = Platform.RLEEncode(rawData.ToList()); - string fileName = $"{Guid.NewGuid():N}.PRReplay"; - string fullPath = Path.Combine(folderPath, fileName); + var fileName = $"{Guid.NewGuid():N}.PRReplay"; + var fullPath = Path.Combine(folderPath, fileName); File.WriteAllBytes(fullPath, compressed.ToArray()); return fullPath; @@ -67,7 +62,7 @@ public void LoadFromFile(string filePath) var file = MessagePackSerializer.Deserialize(rawData.ToArray()); - Init(file.InputSize, file.InitialState, file.MetaData); + Init(file.InputSize, file.InitialState); SetData(file.NumFrames, file.InputFrames); } } diff --git a/src/synchronization/StateStorage.cs b/src/synchronization/StateStorage.cs index b451ff7..7cba1d8 100644 --- a/src/synchronization/StateStorage.cs +++ b/src/synchronization/StateStorage.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace PleaseResync.synchronization +namespace PleaseResync.Synchronization { internal class StateStorage { diff --git a/src/synchronization/Sync.cs b/src/synchronization/Sync.cs index 442da77..dc32068 100644 --- a/src/synchronization/Sync.cs +++ b/src/synchronization/Sync.cs @@ -1,8 +1,10 @@ using System.Diagnostics; using System.Collections.Generic; using System; -using PleaseResync.input; -using PleaseResync.session; +using PleaseResync.Input; +using PleaseResync.Session; +using PleaseResync.Synchronization; +using PleaseResync.Session.Backends.Utility; namespace PleaseResync.synchronization { @@ -28,6 +30,8 @@ public enum SyncState { SYNCING, RUNNING, DEVICE_LOST, DESYNCED } private uint _lastSentChecksum; private uint[] rollbackFrames; + private BroadcastStream _broadcastStream; + public Sync(Device[] devices, uint inputSize, bool offline, List spectators = null) { _devices = devices; @@ -39,6 +43,7 @@ public Sync(Device[] devices, uint inputSize, bool offline, List spectat _syncState = SyncState.SYNCING; _spectators = spectators ?? new List(); rollbackFrames = new uint[16]; + _broadcastStream = new BroadcastStream(); } public void AddRemoteInput(uint deviceId, int frame, int advantage, byte[] deviceInput) @@ -85,7 +90,6 @@ public List AdvanceSync(uint localDeviceId, byte[] deviceInput) UpdateSyncFrame(); var actions = new List(); - if (!_offlinePlay) { // create savestate at the initialFrame to support rolling back to it @@ -95,6 +99,13 @@ public List AdvanceSync(uint localDeviceId, byte[] deviceInput) actions.Add(new SessionSaveGameAction(_timeSync.LocalFrame, _stateStorage)); } + // for replay store the initial gamestate + if (_timeSync.LocalFrame == TimeSync.InitialFrame + 1) + { + var initialState = _stateStorage.LoadFrame(TimeSync.InitialFrame).Buffer; + _broadcastStream.SetInitialState(initialState); + } + // rollback update if (_timeSync.ShouldRollback()) { @@ -157,9 +168,6 @@ private void HandleDisconnectedDevices() private void SendSpectatorInputs() { - // no spectators? dont send inputs - if (_spectators.Count == 0) return; - var maxFrame = _timeSync.SyncFrame; var minFrame = Math.Max(0, maxFrame - (TimeSync.MaxRollbackFrames - 1)); @@ -173,7 +181,7 @@ private void SendSpectatorInputs() } } - if(minAck != int.MaxValue) + if (minAck != int.MaxValue) { minFrame = Math.Max(minFrame, minAck); } @@ -182,7 +190,10 @@ private void SendSpectatorInputs() var sendInput = new List(); for (var i = minFrame; i <= maxFrame; i++) { - sendInput.AddRange(GetFrameInput(i).Inputs); + var inputs = GetFrameInput(i).Inputs; + + sendInput.AddRange(inputs); + _broadcastStream.AddFrameInput(i, inputs); } foreach (var spectator in _spectators) @@ -411,5 +422,6 @@ public GameInput GetFrameInput(int frame) public uint RollbackFrames() => (uint)Math.Max(0, _timeSync.LocalFrame - (_timeSync.SyncFrame + 1)); public uint AverageRollbackFrames() => GetAverageRollbackFrames(); public SyncState State() => _syncState; + public void SaveToReplayFile() => _broadcastStream.SaveReplayFile(); } } diff --git a/src/synchronization/TimeSync.cs b/src/synchronization/TimeSync.cs index d30b73e..7943140 100644 --- a/src/synchronization/TimeSync.cs +++ b/src/synchronization/TimeSync.cs @@ -1,6 +1,6 @@ -using PleaseResync.session; +using PleaseResync.Session; -namespace PleaseResync.synchronization +namespace PleaseResync.Synchronization { internal class TimeSync { From e415692facff24eac61462b44bfa739bacf881cc Mon Sep 17 00:00:00 2001 From: Jamie Meyer <45072324+HeatXD@users.noreply.github.com> Date: Thu, 5 Jun 2025 06:26:29 +0000 Subject: [PATCH 3/5] ? didnt want new dir --- src/Session/Backends/ReplaySession.cs | 220 -------------------------- 1 file changed, 220 deletions(-) delete mode 100644 src/Session/Backends/ReplaySession.cs diff --git a/src/Session/Backends/ReplaySession.cs b/src/Session/Backends/ReplaySession.cs deleted file mode 100644 index 667a3b0..0000000 --- a/src/Session/Backends/ReplaySession.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using PleaseResync.Session.Backends.Utility; -using PleaseResync.Synchronization; - -namespace PleaseResync.Session.Backends -{ - public class ReplaySession : Session - { - public enum ReplayState - { - NoFile, - FileLoaded, - Restarting, - Running, - Paused, - Stepping, - Jumping, - } - - private ReplayState _state; - private int _currentFrame = 0; - private int _requestedFrame = -1; - private readonly StateStorage _stateStorage; - private readonly BroadcastStream _broadcastStream; - - public ReplaySession() : base(0, 0, 0, false, true) - { - _state = ReplayState.NoFile; - _broadcastStream = new BroadcastStream(); - _stateStorage = new StateStorage(0); - } - - public void LoadFile(string filepath) - { - _broadcastStream.LoadReplayFile(filepath); - - _state = ReplayState.FileLoaded; - - var initialState = _broadcastStream.GetInitialState(); - _stateStorage.SaveFrame(0, initialState); - } - - public void Restart() - { - if (_state == ReplayState.NoFile) return; - - _state = ReplayState.Restarting; - } - - public void Pause() - { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Paused; - } - - public void Resume() - { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Running; - } - - public void Step() - { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Stepping; - _requestedFrame = _currentFrame + 1; - } - - public void GoToFrame(int frame) - { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Jumping; - _requestedFrame = frame; - } - - - protected internal override Device LocalDevice => throw new System.NotImplementedException(); - - protected internal override Device[] AllDevices => throw new System.NotImplementedException(); - - public override void AddRemoteDevice(uint deviceId, uint playerCount, object remoteConfiguration) - { - throw new System.NotImplementedException(); - } - - public override void AddSpectatorDevice(object remoteConfiguration) - { - throw new System.NotImplementedException(); - } - - public override List AdvanceFrame(byte[] localInput = null) - { - var actions = new List(); - - switch (_state) - { - case ReplayState.NoFile: - break; - case ReplayState.FileLoaded: - case ReplayState.Restarting: - _currentFrame = 0; - _broadcastStream.SetCurrentFrame(0); - Resume(); - break; - case ReplayState.Running: - if (_broadcastStream.GetFrameInput(out var frame, out var input)) - { - if (_currentFrame == 0) - { - actions.Add(new SessionLoadGameAction(0, _stateStorage)); - } - _currentFrame = frame; - actions.Add(new SessionAdvanceFrameAction(frame, input)); - } - break; - case ReplayState.Stepping: - if (_currentFrame + 1 == _requestedFrame && - _broadcastStream.GetFrameInput(out var stepFrame, out var stepInput)) - { - _currentFrame = stepFrame; - actions.Add(new SessionAdvanceFrameAction(stepFrame, stepInput)); - } - break; - case ReplayState.Jumping: - if (_currentFrame > _requestedFrame) - { - actions.Add(new SessionLoadGameAction(0, _stateStorage)); - _currentFrame = 0; - } - _broadcastStream.SetCurrentFrame(_currentFrame); - for (int i = _currentFrame; i <= _requestedFrame; i++) - { - if (_broadcastStream.GetFrameInput(out var jumpFrame, out var jumpInput)) - { - _currentFrame = jumpFrame; - actions.Add(new SessionAdvanceFrameAction(jumpFrame, jumpInput)); - } - } - Resume(); - break; - case ReplayState.Paused: - break; - default: - break; - } - - return actions; - } - - public override uint AverageRollbackFrames() - { - throw new System.NotImplementedException(); - } - - public override int Frame() - { - return _currentFrame; - } - - public override int FrameAdvantage() - { - throw new System.NotImplementedException(); - } - - public override int FrameAdvantageDifference() - { - throw new System.NotImplementedException(); - } - - public override bool IsRunning() - { - return _state != ReplayState.NoFile; - } - - public override void Poll() - { - } - - public override int RemoteFrame() - { - throw new System.NotImplementedException(); - } - - public override int RemoteFrameAdvantage() - { - throw new System.NotImplementedException(); - } - - public override uint RollbackFrames() - { - throw new System.NotImplementedException(); - } - - public override void SetLocalDevice(uint deviceId, uint playerCount, uint frameDelay) - { - throw new System.NotImplementedException(); - } - - public override int State() - { - throw new System.NotImplementedException(); - } - - protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessage message) - { - throw new System.NotImplementedException(); - } - - protected internal override uint SendMessageTo(uint deviceId, DeviceMessage message) - { - throw new System.NotImplementedException(); - } - - public override void SaveToReplayFile() - { - throw new NotImplementedException(); - } - } -} From 083fedd98826759c2e84a5d9ddd6d21bf4f7dcc9 Mon Sep 17 00:00:00 2001 From: Jamie Meyer <45072324+HeatXD@users.noreply.github.com> Date: Thu, 5 Jun 2025 06:27:39 +0000 Subject: [PATCH 4/5] move to right dir --- src/session/backends/ReplaySession.cs | 220 ++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/session/backends/ReplaySession.cs diff --git a/src/session/backends/ReplaySession.cs b/src/session/backends/ReplaySession.cs new file mode 100644 index 0000000..667a3b0 --- /dev/null +++ b/src/session/backends/ReplaySession.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using PleaseResync.Session.Backends.Utility; +using PleaseResync.Synchronization; + +namespace PleaseResync.Session.Backends +{ + public class ReplaySession : Session + { + public enum ReplayState + { + NoFile, + FileLoaded, + Restarting, + Running, + Paused, + Stepping, + Jumping, + } + + private ReplayState _state; + private int _currentFrame = 0; + private int _requestedFrame = -1; + private readonly StateStorage _stateStorage; + private readonly BroadcastStream _broadcastStream; + + public ReplaySession() : base(0, 0, 0, false, true) + { + _state = ReplayState.NoFile; + _broadcastStream = new BroadcastStream(); + _stateStorage = new StateStorage(0); + } + + public void LoadFile(string filepath) + { + _broadcastStream.LoadReplayFile(filepath); + + _state = ReplayState.FileLoaded; + + var initialState = _broadcastStream.GetInitialState(); + _stateStorage.SaveFrame(0, initialState); + } + + public void Restart() + { + if (_state == ReplayState.NoFile) return; + + _state = ReplayState.Restarting; + } + + public void Pause() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Paused; + } + + public void Resume() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Running; + } + + public void Step() + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Stepping; + _requestedFrame = _currentFrame + 1; + } + + public void GoToFrame(int frame) + { + if (_state == ReplayState.NoFile) return; + _state = ReplayState.Jumping; + _requestedFrame = frame; + } + + + protected internal override Device LocalDevice => throw new System.NotImplementedException(); + + protected internal override Device[] AllDevices => throw new System.NotImplementedException(); + + public override void AddRemoteDevice(uint deviceId, uint playerCount, object remoteConfiguration) + { + throw new System.NotImplementedException(); + } + + public override void AddSpectatorDevice(object remoteConfiguration) + { + throw new System.NotImplementedException(); + } + + public override List AdvanceFrame(byte[] localInput = null) + { + var actions = new List(); + + switch (_state) + { + case ReplayState.NoFile: + break; + case ReplayState.FileLoaded: + case ReplayState.Restarting: + _currentFrame = 0; + _broadcastStream.SetCurrentFrame(0); + Resume(); + break; + case ReplayState.Running: + if (_broadcastStream.GetFrameInput(out var frame, out var input)) + { + if (_currentFrame == 0) + { + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + } + _currentFrame = frame; + actions.Add(new SessionAdvanceFrameAction(frame, input)); + } + break; + case ReplayState.Stepping: + if (_currentFrame + 1 == _requestedFrame && + _broadcastStream.GetFrameInput(out var stepFrame, out var stepInput)) + { + _currentFrame = stepFrame; + actions.Add(new SessionAdvanceFrameAction(stepFrame, stepInput)); + } + break; + case ReplayState.Jumping: + if (_currentFrame > _requestedFrame) + { + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + _currentFrame = 0; + } + _broadcastStream.SetCurrentFrame(_currentFrame); + for (int i = _currentFrame; i <= _requestedFrame; i++) + { + if (_broadcastStream.GetFrameInput(out var jumpFrame, out var jumpInput)) + { + _currentFrame = jumpFrame; + actions.Add(new SessionAdvanceFrameAction(jumpFrame, jumpInput)); + } + } + Resume(); + break; + case ReplayState.Paused: + break; + default: + break; + } + + return actions; + } + + public override uint AverageRollbackFrames() + { + throw new System.NotImplementedException(); + } + + public override int Frame() + { + return _currentFrame; + } + + public override int FrameAdvantage() + { + throw new System.NotImplementedException(); + } + + public override int FrameAdvantageDifference() + { + throw new System.NotImplementedException(); + } + + public override bool IsRunning() + { + return _state != ReplayState.NoFile; + } + + public override void Poll() + { + } + + public override int RemoteFrame() + { + throw new System.NotImplementedException(); + } + + public override int RemoteFrameAdvantage() + { + throw new System.NotImplementedException(); + } + + public override uint RollbackFrames() + { + throw new System.NotImplementedException(); + } + + public override void SetLocalDevice(uint deviceId, uint playerCount, uint frameDelay) + { + throw new System.NotImplementedException(); + } + + public override int State() + { + throw new System.NotImplementedException(); + } + + protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessage message) + { + throw new System.NotImplementedException(); + } + + protected internal override uint SendMessageTo(uint deviceId, DeviceMessage message) + { + throw new System.NotImplementedException(); + } + + public override void SaveToReplayFile() + { + throw new NotImplementedException(); + } + } +} From d721b00cb36848fa0465683e42063550e622a87f Mon Sep 17 00:00:00 2001 From: Jamie Meyer <45072324+HeatXD@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:31:32 +0000 Subject: [PATCH 5/5] Move to a command based structure --- src/session/backends/ReplaySession.cs | 270 ++++++++++++-------------- 1 file changed, 129 insertions(+), 141 deletions(-) diff --git a/src/session/backends/ReplaySession.cs b/src/session/backends/ReplaySession.cs index 667a3b0..0a07635 100644 --- a/src/session/backends/ReplaySession.cs +++ b/src/session/backends/ReplaySession.cs @@ -5,216 +5,204 @@ namespace PleaseResync.Session.Backends { - public class ReplaySession : Session + public sealed class ReplaySession : Session { - public enum ReplayState + private enum PlaybackState { NoFile, FileLoaded, - Restarting, Running, - Paused, - Stepping, - Jumping, + Paused } - private ReplayState _state; - private int _currentFrame = 0; - private int _requestedFrame = -1; - private readonly StateStorage _stateStorage; + private readonly Queue>> _commandQueue = new(); private readonly BroadcastStream _broadcastStream; + private readonly StateStorage _stateStorage; + + private PlaybackState _currentState = PlaybackState.NoFile; + private int _currentFrame; + private int _targetFrame; - public ReplaySession() : base(0, 0, 0, false, true) + public ReplaySession(): base(0, 0, 0, false, true) { - _state = ReplayState.NoFile; _broadcastStream = new BroadcastStream(); _stateStorage = new StateStorage(0); + _currentFrame = 0; + _targetFrame = -1; } - public void LoadFile(string filepath) + public void LoadFile(string filePath) { - _broadcastStream.LoadReplayFile(filepath); - - _state = ReplayState.FileLoaded; + Enqueue(actions => + { + _broadcastStream.LoadReplayFile(filePath); + var initialState = _broadcastStream.GetInitialState(); + _stateStorage.SaveFrame(0, initialState); + + _currentFrame = 0; + _targetFrame = -1; + _currentState = PlaybackState.FileLoaded; - var initialState = _broadcastStream.GetInitialState(); - _stateStorage.SaveFrame(0, initialState); + // Immediately emit a load-game action so the engine knows to load state 0 + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + }); } public void Restart() { - if (_state == ReplayState.NoFile) return; + Enqueue(actions => + { + if (_currentState == PlaybackState.NoFile) return; + + _currentFrame = 0; + _broadcastStream.SetCurrentFrame(0); + _currentState = PlaybackState.Running; - _state = ReplayState.Restarting; + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + }); } public void Pause() { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Paused; + Enqueue(actions => + { + if (_currentState != PlaybackState.NoFile) + _currentState = PlaybackState.Paused; + }); } public void Resume() { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Running; + Enqueue(actions => + { + if (_currentState == PlaybackState.NoFile) return; + _currentState = PlaybackState.Running; + }); } - + public void Step() { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Stepping; - _requestedFrame = _currentFrame + 1; + Enqueue(actions => + { + if (_currentState == PlaybackState.NoFile) return; + + if (_broadcastStream.GetFrameInput(out var frame, out var input)) + { + _currentFrame = frame; + actions.Add(new SessionAdvanceFrameAction(frame, input)); + } + + _currentState = PlaybackState.Paused; + }); } public void GoToFrame(int frame) { - if (_state == ReplayState.NoFile) return; - _state = ReplayState.Jumping; - _requestedFrame = frame; - } + Enqueue(actions => + { + if (_currentState == PlaybackState.NoFile) return; + _targetFrame = frame; + if (_currentFrame > _targetFrame) + { + actions.Add(new SessionLoadGameAction(0, _stateStorage)); + _currentFrame = 0; + } - protected internal override Device LocalDevice => throw new System.NotImplementedException(); + _broadcastStream.SetCurrentFrame(_currentFrame); - protected internal override Device[] AllDevices => throw new System.NotImplementedException(); + while (_currentFrame < _targetFrame) + { + if (!_broadcastStream.GetFrameInput(out var nextFrame, out var nextInput)) + break; - public override void AddRemoteDevice(uint deviceId, uint playerCount, object remoteConfiguration) - { - throw new System.NotImplementedException(); - } + _currentFrame = nextFrame; + actions.Add(new SessionAdvanceFrameAction(nextFrame, nextInput)); + } - public override void AddSpectatorDevice(object remoteConfiguration) - { - throw new System.NotImplementedException(); + _currentState = PlaybackState.Paused; + _targetFrame = -1; + }); } + private void Enqueue(Action> command) => + _commandQueue.Enqueue(command); + public override List AdvanceFrame(byte[] localInput = null) { var actions = new List(); - switch (_state) + while (_commandQueue.Count > 0) { - case ReplayState.NoFile: - break; - case ReplayState.FileLoaded: - case ReplayState.Restarting: - _currentFrame = 0; - _broadcastStream.SetCurrentFrame(0); - Resume(); - break; - case ReplayState.Running: - if (_broadcastStream.GetFrameInput(out var frame, out var input)) - { - if (_currentFrame == 0) - { - actions.Add(new SessionLoadGameAction(0, _stateStorage)); - } - _currentFrame = frame; - actions.Add(new SessionAdvanceFrameAction(frame, input)); - } - break; - case ReplayState.Stepping: - if (_currentFrame + 1 == _requestedFrame && - _broadcastStream.GetFrameInput(out var stepFrame, out var stepInput)) - { - _currentFrame = stepFrame; - actions.Add(new SessionAdvanceFrameAction(stepFrame, stepInput)); - } - break; - case ReplayState.Jumping: - if (_currentFrame > _requestedFrame) - { - actions.Add(new SessionLoadGameAction(0, _stateStorage)); - _currentFrame = 0; - } - _broadcastStream.SetCurrentFrame(_currentFrame); - for (int i = _currentFrame; i <= _requestedFrame; i++) - { - if (_broadcastStream.GetFrameInput(out var jumpFrame, out var jumpInput)) - { - _currentFrame = jumpFrame; - actions.Add(new SessionAdvanceFrameAction(jumpFrame, jumpInput)); - } - } - Resume(); - break; - case ReplayState.Paused: - break; - default: - break; + var cmd = _commandQueue.Dequeue(); + cmd.Invoke(actions); + } + + if (_currentState == PlaybackState.Running) + { + if (_broadcastStream.GetFrameInput(out var frame, out var input)) + { + _currentFrame = frame; + actions.Add(new SessionAdvanceFrameAction(frame, input)); + } } return actions; } - public override uint AverageRollbackFrames() - { - throw new System.NotImplementedException(); - } + protected internal override Device LocalDevice => + throw new NotImplementedException(); - public override int Frame() - { - return _currentFrame; - } + protected internal override Device[] AllDevices => + throw new NotImplementedException(); - public override int FrameAdvantage() - { - throw new System.NotImplementedException(); - } + public override void AddRemoteDevice(uint deviceId, uint playerCount, object remoteConfiguration) => + throw new NotImplementedException(); - public override int FrameAdvantageDifference() - { - throw new System.NotImplementedException(); - } + public override void AddSpectatorDevice(object remoteConfiguration) => + throw new NotImplementedException(); - public override bool IsRunning() - { - return _state != ReplayState.NoFile; - } + public override uint AverageRollbackFrames() => + throw new NotImplementedException(); + + public override int Frame() => _currentFrame; + + public override int FrameAdvantage() => + throw new NotImplementedException(); + + public override int FrameAdvantageDifference() => + throw new NotImplementedException(); + + public override bool IsRunning() => + _currentState != PlaybackState.NoFile; public override void Poll() { + // No-op for replay } - public override int RemoteFrame() - { - throw new System.NotImplementedException(); - } + public override int RemoteFrame() => + throw new NotImplementedException(); - public override int RemoteFrameAdvantage() - { - throw new System.NotImplementedException(); - } + public override int RemoteFrameAdvantage() => + throw new NotImplementedException(); - public override uint RollbackFrames() - { - throw new System.NotImplementedException(); - } + public override uint RollbackFrames() => + throw new NotImplementedException(); - public override void SetLocalDevice(uint deviceId, uint playerCount, uint frameDelay) - { - throw new System.NotImplementedException(); - } + public override void SetLocalDevice(uint deviceId, uint playerCount, uint frameDelay) => + throw new NotImplementedException(); - public override int State() - { - throw new System.NotImplementedException(); - } + public override int State() => + throw new NotImplementedException(); - protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessage message) - { - throw new System.NotImplementedException(); - } + protected internal override void AddRemoteInput(uint deviceId, DeviceInputMessage message) => + throw new NotImplementedException(); - protected internal override uint SendMessageTo(uint deviceId, DeviceMessage message) - { - throw new System.NotImplementedException(); - } + protected internal override uint SendMessageTo(uint deviceId, DeviceMessage message) => + throw new NotImplementedException(); - public override void SaveToReplayFile() - { + public override void SaveToReplayFile() => throw new NotImplementedException(); - } } }