diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 93b7361a5db..7e119ec7b7d 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -43,7 +43,7 @@ END TEMPLATE-->
### Bugfixes
-*None yet*
+* Fixed a state handling bug in replays, which was causing exceptions to be thrown when applying delta states.
### Other
diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs
index 30240d7abb5..bb30ea4ddc5 100644
--- a/Robust.Client/GameStates/ClientGameStateManager.cs
+++ b/Robust.Client/GameStates/ClientGameStateManager.cs
@@ -399,7 +399,7 @@ public void ApplyGameState()
using (_prof.Group("MergeImplicitData"))
{
- MergeImplicitData(createdEntities);
+ GenerateImplicitStates(createdEntities);
}
if (_lastProcessedInput < curState.LastProcessedInput)
@@ -671,7 +671,7 @@ public void ResetPredictedEntities()
/// initial server state for any newly created entity. It does this by simply using the standard .
///
- private void MergeImplicitData(IEnumerable createdEntities)
+ public void GenerateImplicitStates(IEnumerable createdEntities)
{
var bus = _entityManager.EventBus;
diff --git a/Robust.Client/GameStates/IClientGameStateManager.cs b/Robust.Client/GameStates/IClientGameStateManager.cs
index fc50155b693..2e798582d59 100644
--- a/Robust.Client/GameStates/IClientGameStateManager.cs
+++ b/Robust.Client/GameStates/IClientGameStateManager.cs
@@ -82,6 +82,12 @@ public interface IClientGameStateManager
///
IEnumerable ApplyGameState(GameState curState, GameState? nextState);
+ ///
+ /// Generates implicit component states for newly created entities.
+ /// This should always be called after running .
+ ///
+ void GenerateImplicitStates(IEnumerable states);
+
///
/// Resets any entities that have changed while predicting future ticks.
///
diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Start.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Start.cs
index 42728535f05..e319e7505e3 100644
--- a/Robust.Client/Replays/Loading/ReplayLoadManager.Start.cs
+++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Start.cs
@@ -95,9 +95,21 @@ public async Task StartReplayAsync(ReplayData data, LoadReplayCallback callback)
_gameState.ClearDetachQueue();
_gameState.ApplyGameState(checkpoint.State, next);
+ // Sort entities to ensure that we initialize parents before children
+ var sorted = new List(entities.Count);
+ var added = new HashSet(entities.Count);
+ var xformQuery = _entMan.GetEntityQuery();
+ foreach (var uid in entities)
+ {
+ AddSorted(uid, sorted, added, xformQuery);
+ }
+ DebugTools.AssertEqual(sorted.Count, entities.Count);
+ DebugTools.AssertEqual(added.Count, entities.Count);
+ await callback(i, total, LoadingState.Initializing, false);
+
i = 0;
var query = _entMan.GetEntityQuery();
- foreach (var uid in entities)
+ foreach (var uid in sorted)
{
_entMan.InitializeEntity(uid, query.GetComponent(uid));
if (i++ % 50 == 0)
@@ -109,7 +121,7 @@ public async Task StartReplayAsync(ReplayData data, LoadReplayCallback callback)
i = 0;
await callback(0, total, LoadingState.Starting, true);
- foreach (var uid in entities)
+ foreach (var uid in sorted)
{
_entMan.StartEntity(uid);
if (i++ % 50 == 0)
@@ -132,4 +144,16 @@ public async Task StartReplayAsync(ReplayData data, LoadReplayCallback callback)
_replayPlayback.StartReplay(data);
_timing.Paused = false;
}
+
+ private void AddSorted(EntityUid uid, List sortedList, HashSet added, EntityQuery query)
+ {
+ if (!added.Add(uid))
+ return;
+
+ var parent = query.Comp(uid).ParentUid;
+ if (parent != EntityUid.Invalid)
+ AddSorted(parent, sortedList, added, query);
+
+ sortedList.Add(uid);
+ }
}
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
index 6fb60f36e54..2efa8f32fba 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Update.cs
@@ -44,7 +44,8 @@ private void TickUpdateOverride(FrameEventArgs args)
_gameState.UpdateFullRep(state, cloneDelta: true);
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
- _gameState.ApplyGameState(state, next);
+ var created = _gameState.ApplyGameState(state, next);
+ _gameState.GenerateImplicitStates(created);
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;
diff --git a/Robust.Shared/GameObjects/ComponentState.cs b/Robust.Shared/GameObjects/ComponentState.cs
index 8f80d51f218..ee9e5740313 100644
--- a/Robust.Shared/GameObjects/ComponentState.cs
+++ b/Robust.Shared/GameObjects/ComponentState.cs
@@ -40,17 +40,17 @@ public interface IComponentDeltaState : IComponentDeltaState where TStat
void IComponentDeltaState.ApplyToFullState(IComponentState fullState)
{
- if (fullState is TState state)
- ApplyToFullState(state);
- else
- throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
+ if (fullState is not TState state)
+ throw new Exception($"Unexpected type. Expected {typeof(TState).Name} but got {fullState.GetType().Name}");
+
+ ApplyToFullState(state);
}
IComponentState IComponentDeltaState.CreateNewFullState(IComponentState fullState)
{
- if (fullState is TState state)
- return CreateNewFullState(state);
- else
- throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
+ if (fullState is not TState state)
+ throw new Exception($"Unexpected type. Expected {typeof(TState).Name} but got {fullState.GetType().Name}");
+
+ return CreateNewFullState(state);
}
}