@@ -31,6 +31,7 @@ public sealed partial class ClientEntityManager : EntityManager, IClientEntityMa
3131
3232 private readonly Queue < EntityUid > _queuedPredictedDeletions = new ( ) ;
3333 private readonly HashSet < EntityUid > _queuedPredictedDeletionsSet = new ( ) ;
34+ private readonly HashSet < EntityUid > _predictedDetachedEntities = new ( ) ;
3435 private Histogram ? _tickUpdateHistogram ;
3536 private Histogram . Child ? _entityNetHistogram ;
3637
@@ -61,6 +62,9 @@ public override void FlushEntities()
6162 // Server doesn't network deletions on client shutdown so we need to
6263 // manually clear these out or risk stale data getting used.
6364 PendingNetEntityStates . Clear ( ) ;
65+ _queuedPredictedDeletions . Clear ( ) ;
66+ _queuedPredictedDeletionsSet . Clear ( ) ;
67+ _predictedDetachedEntities . Clear ( ) ;
6468 using var _ = _gameTiming . StartStateApplicationArea ( ) ;
6569 base . FlushEntities ( ) ;
6670 }
@@ -97,6 +101,14 @@ public override void QueueDeleteEntity(EntityUid? uid)
97101 LogManager . RootSawmill . Error ( $ "Predicting the queued deletion of a networked entity: { ToPrettyString ( uid . Value ) } . Trace: { Environment . StackTrace } ") ;
98102 }
99103
104+ public override void DeleteEntity ( EntityUid ? uid )
105+ {
106+ if ( uid != null )
107+ ClearPredictedDeletion ( uid . Value ) ;
108+
109+ base . DeleteEntity ( uid ) ;
110+ }
111+
100112 /// <inheritdoc />
101113 public override void Dirty ( EntityUid uid , IComponent component , MetaDataComponent ? meta = null )
102114 {
@@ -237,6 +249,9 @@ internal override void ProcessQueueudDeletions()
237249 base . ProcessQueueudDeletions ( ) ;
238250 while ( _queuedPredictedDeletions . TryDequeue ( out var uid ) )
239251 {
252+ if ( ! _queuedPredictedDeletionsSet . Remove ( uid ) )
253+ continue ;
254+
240255 if ( ! MetaQuery . TryGetComponentInternal ( uid , out var meta ) )
241256 continue ;
242257
@@ -246,19 +261,14 @@ internal override void ProcessQueueudDeletions()
246261 var xform = TransformQuery . GetComponentInternal ( uid ) ;
247262 if ( meta . NetEntity . IsClientSide ( ) )
248263 {
264+ ClearPredictedDeletion ( uid ) ;
249265 DeleteEntity ( uid , meta , xform ) ;
250266 }
251267 else
252268 {
253- _xforms . DetachEntity ( uid , xform , meta , null ) ;
254- // base call bypasses IGameTiming.InPrediction check
255- // This is pretty janky and there should be a way for the client to dirty an entity outside of prediction
256- // TODO PREDICTION Is actually needed after the current predicted deletion fix?
257- base . Dirty ( uid , xform , meta ) ;
269+ PredictedDetachNetworkedEntity ( uid , xform , meta ) ;
258270 }
259271 }
260-
261- _queuedPredictedDeletionsSet . Clear ( ) ;
262272 }
263273
264274 /// <inheritdoc />
@@ -354,16 +364,42 @@ public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformC
354364
355365 if ( ent . Comp1 . NetEntity . IsClientSide ( ) )
356366 {
367+ ClearPredictedDeletion ( ent . Owner ) ;
357368 DeleteEntity ( ent , ent . Comp1 , ent . Comp2 ) ;
358369 }
359370 else
360371 {
361- _xforms . DetachEntity ( ent , ent . Comp2 ) ;
372+ PredictedDetachNetworkedEntity ( ent . Owner , ent . Comp2 , ent . Comp1 ) ;
362373 }
363374 }
364375
376+ internal bool IsPredictedDetached ( EntityUid uid )
377+ {
378+ return _predictedDetachedEntities . Contains ( uid ) ;
379+ }
380+
381+ internal void ClearPredictedDeletion ( EntityUid uid )
382+ {
383+ _predictedDetachedEntities . Remove ( uid ) ;
384+ _queuedPredictedDeletionsSet . Remove ( uid ) ;
385+ }
386+
387+ private void PredictedDetachNetworkedEntity ( EntityUid uid , TransformComponent xform , MetaDataComponent meta )
388+ {
389+ if ( ! _predictedDetachedEntities . Add ( uid ) )
390+ return ;
391+
392+ // base call bypasses IGameTiming.InPrediction check. Predicted queue deletes are processed after prediction,
393+ // but reset still needs to see the detached entity as dirty and restore it from the last server state.
394+ base . Dirty ( uid , xform , meta ) ;
395+ _xforms . DetachEntity ( uid , xform , meta , null ) ;
396+ meta . Flags |= MetaDataFlags . Detached ;
397+ }
398+
365399 public override bool IsQueuedForDeletion ( EntityUid uid )
366- => QueuedDeletionsSet . Contains ( uid ) || _queuedPredictedDeletions . Contains ( uid ) ;
400+ => QueuedDeletionsSet . Contains ( uid )
401+ || _queuedPredictedDeletionsSet . Contains ( uid )
402+ || _predictedDetachedEntities . Contains ( uid ) ;
367403
368404 /// <inheritdoc />
369405 public override void PredictedQueueDeleteEntity ( Entity < MetaDataComponent ? > ent )
0 commit comments