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); } }