From bb4c4ed3025d3fd89b9005fe996c74ef681355d2 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:04:12 +1300 Subject: [PATCH] Fix PredictedQueueDeleteEntity mispredicts (#6260) * Fix PredictedQueueDeleteEntity * typo --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- RELEASE-NOTES.md | 1 + .../GameObjects/ClientEntityManager.cs | 49 ++++++++++--------- .../GameStates/ClientGameStateManager.cs | 5 ++ Robust.Shared/GameObjects/EntityManager.cs | 17 ++++--- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 05aee74aa..666c585fb 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -50,6 +50,7 @@ END TEMPLATE--> ### Bugfixes * Fix `Menu` and `NumpadDecimal` key codes on SDL3. +* client-side predicted entity deletion ( `EntityManager.PredictedQueueDeleteEntity`) now behaves more like it does on the server. In particular, entities will be deleted on the same tick after all system have been updated. Previously, it would process deletions at the beginning of the next tick. * Fix modifying `Label.FontOverride` not causing a layout update. * Controls created by rich-text tags now get arranged to a proper size. * Fix `OutputPanel` scrollbar breaking if a style update changes the font size. diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index feac45b13..a86e432a9 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -216,35 +216,36 @@ namespace Robust.Client.GameObjects } } - using (histogram?.WithLabels("PredictedQueueDel").NewTimer()) + base.TickUpdate(frameTime, noPredictions, histogram); + } + + internal override void ProcessQueueudDeletions() + { + base.ProcessQueueudDeletions(); + while (_queuedPredictedDeletions.TryDequeue(out var uid)) { - while (_queuedPredictedDeletions.TryDequeue(out var uid)) + if (!MetaQuery.TryGetComponentInternal(uid, out var meta)) + continue; + + if (meta.EntityLifeStage >= EntityLifeStage.Terminating) + continue; + + var xform = TransformQuery.GetComponentInternal(uid); + if (meta.NetEntity.IsClientSide()) { - if (!MetaQuery.TryGetComponentInternal(uid, out var meta)) - continue; - - if (meta.EntityLifeStage >= EntityLifeStage.Terminating) - continue; - - var xform = TransformQuery.GetComponentInternal(uid); - if (meta.NetEntity.IsClientSide()) - { - DeleteEntity(uid, meta, xform); - } - else - { - _xforms.DetachEntity(uid, xform, meta, null); - // base call bypasses IGameTiming.InPrediction check - // This is pretty janky and there should be a way for the client to dirty an entity outside of prediction - // TODO PREDICTION - base.Dirty(uid, xform, meta); - } + DeleteEntity(uid, meta, xform); + } + else + { + _xforms.DetachEntity(uid, xform, meta, null); + // base call bypasses IGameTiming.InPrediction check + // This is pretty janky and there should be a way for the client to dirty an entity outside of prediction + // TODO PREDICTION Is actually needed after the current predicted deletion fix? + base.Dirty(uid, xform, meta); } - - _queuedPredictedDeletionsSet.Clear(); } - base.TickUpdate(frameTime, noPredictions, histogram); + _queuedPredictedDeletionsSet.Clear(); } /// diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index bbd19d127..3767d3e43 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -544,6 +544,11 @@ namespace Robust.Client.GameStates { ((IBroadcastEventBusInternal)_entities.EventBus).ProcessEventQueue(); } + + using (_prof.Group("QueueDel")) + { + _entities.ProcessQueueudDeletions(); + } } _prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(_timing.CurTick.Value)); diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 5fa00fe17..bf2d802bb 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -288,12 +288,7 @@ namespace Robust.Shared.GameObjects using (histogram?.WithLabels("QueuedDeletion").NewTimer()) using (_prof.Group("QueueDel")) { - while (QueuedDeletions.TryDequeue(out var uid)) - { - DeleteEntity(uid); - } - - QueuedDeletionsSet.Clear(); + ProcessQueueudDeletions(); } using (histogram?.WithLabels("ComponentCull").NewTimer()) @@ -303,6 +298,16 @@ namespace Robust.Shared.GameObjects } } + internal virtual void ProcessQueueudDeletions() + { + while (QueuedDeletions.TryDequeue(out var uid)) + { + DeleteEntity(uid); + } + + QueuedDeletionsSet.Clear(); + } + public virtual void FrameUpdate(float frameTime) { _entitySystemManager.FrameUpdate(frameTime);