fix(streaming): #138 — rescue persistent entities from the pending bucket on unload

GpuWorldState.RemoveLandblock rescued persistent entities (the player)
only from the _loaded list, silently dropping one sitting in the
_pendingByLandblock bucket. The player is re-injected via AppendLiveEntity
every frame; right after a teleport its destination landblock has not
streamed in yet, so the player lands in the pending bucket — and if that
landblock is then unloaded during the streaming churn, the persistent
entry was dropped, violating the "persistent therefore survives unload"
contract. Leading candidate for the #138 "own avatar vanishes after a
couple round-trips" symptom (cumulative; needs user visual confirm).

Fix: scan the pending bucket for persistent guids and rescue them too,
so DrainRescued re-parks them at the next valid landblock. Provable
correctness fix with a deterministic test (rescue-from-pending plus a
negative for non-persistent). Core tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 08:07:28 +02:00
parent bf66fb4123
commit 0a5f91b6fe
2 changed files with 67 additions and 0 deletions

View file

@ -315,6 +315,26 @@ public sealed class GpuWorldState
}
}
// #138 (secondary): rescue persistent entities sitting in the PENDING
// bucket too, not just the loaded list. The player is re-injected via
// AppendLiveEntity into its current landblock every frame
// (GameWindow's DrainRescued loop); right after a teleport that
// landblock often hasn't streamed in yet, so the player lands in
// _pendingByLandblock. If that same landblock is then unloaded (a
// streaming churn / re-teleport before it finishes loading), the
// pending entry was silently dropped here — violating the
// "persistent ⇒ survives unload" contract and making the avatar
// vanish after a couple round-trips. Rescue them so DrainRescued
// re-parks them at the next valid landblock.
if (_pendingByLandblock.TryGetValue(landblockId, out var pendingForLb))
{
foreach (var entity in pendingForLb)
{
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
_persistentRescued.Add(entity);
}
}
_pendingByLandblock.Remove(landblockId);
_aabbs.Remove(landblockId);