Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix replay playback bugs #5604

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Robust.Client/GameStates/ClientGameStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ public void ApplyGameState()

using (_prof.Group("MergeImplicitData"))
{
MergeImplicitData(createdEntities);
GenerateImplicitStates(createdEntities);
}

if (_lastProcessedInput < curState.LastProcessedInput)
Expand Down Expand Up @@ -671,7 +671,7 @@ public void ResetPredictedEntities()
/// initial server state for any newly created entity. It does this by simply using the standard <see
/// cref="IEntityManager.GetComponentState"/>.
/// </remarks>
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
public void GenerateImplicitStates(IEnumerable<NetEntity> createdEntities)
{
var bus = _entityManager.EventBus;

Expand Down
6 changes: 6 additions & 0 deletions Robust.Client/GameStates/IClientGameStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public interface IClientGameStateManager
/// </summary>
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);

/// <summary>
/// Generates implicit component states for newly created entities.
/// This should always be called after running <see cref="ApplyGameState(GameState, GameState)"/>.
/// </summary>
void GenerateImplicitStates(IEnumerable<NetEntity> states);

/// <summary>
/// Resets any entities that have changed while predicting future ticks.
/// </summary>
Expand Down
28 changes: 26 additions & 2 deletions Robust.Client/Replays/Loading/ReplayLoadManager.Start.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityUid>(entities.Count);
var added = new HashSet<EntityUid>(entities.Count);
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
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<MetaDataComponent>();
foreach (var uid in entities)
foreach (var uid in sorted)
{
_entMan.InitializeEntity(uid, query.GetComponent(uid));
if (i++ % 50 == 0)
Expand All @@ -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)
Expand All @@ -132,4 +144,16 @@ public async Task StartReplayAsync(ReplayData data, LoadReplayCallback callback)
_replayPlayback.StartReplay(data);
_timing.Paused = false;
}

private void AddSorted(EntityUid uid, List<EntityUid> sortedList, HashSet<EntityUid> added, EntityQuery<TransformComponent> query)
{
if (!added.Add(uid))
return;

var parent = query.Comp(uid).ParentUid;
if (parent != EntityUid.Invalid)
AddSorted(parent, sortedList, added, query);

sortedList.Add(uid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +47 to +48
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is what fixes the replay delta state exceptions

DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;
Expand Down
16 changes: 8 additions & 8 deletions Robust.Shared/GameObjects/ComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ public interface IComponentDeltaState<TState> : 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);
}
}
Loading