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:
parent
bf66fb4123
commit
0a5f91b6fe
2 changed files with 67 additions and 0 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -137,4 +137,51 @@ public class GpuWorldStateTests
|
|||
state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu));
|
||||
Assert.True(state.IsLoaded(0xA9B4FFFFu));
|
||||
}
|
||||
|
||||
private static WorldEntity MakeServerEntity(uint id, uint serverGuid)
|
||||
=> new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x01000001u,
|
||||
Position = System.Numerics.Vector3.Zero,
|
||||
Rotation = System.Numerics.Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_RescuesPersistentEntity_FromPendingBucket()
|
||||
{
|
||||
// #138 (secondary): the player is re-injected via AppendLiveEntity every
|
||||
// frame; right after a teleport its landblock hasn't streamed in, so it
|
||||
// lands in the pending bucket. If that landblock is then unloaded before
|
||||
// it finishes loading, the persistent entity must still be rescued —
|
||||
// otherwise the avatar vanishes after a couple round-trips.
|
||||
var state = new GpuWorldState();
|
||||
const uint playerGuid = 0x50000001u;
|
||||
state.MarkPersistent(playerGuid);
|
||||
|
||||
var player = MakeServerEntity(id: 1, serverGuid: playerGuid);
|
||||
state.AppendLiveEntity(0xA9B40011u, player); // landblock not loaded → pending
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu); // unloaded while still pending
|
||||
|
||||
Assert.Contains(player, state.DrainRescued()); // rescued, not dropped
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_DoesNotRescue_NonPersistentPendingEntity()
|
||||
{
|
||||
// A normal server object (door) in the pending bucket is NOT persistent;
|
||||
// it must still be dropped (and is recoverable via #138 re-hydrate from
|
||||
// _lastSpawnByGuid, not via the rescue path).
|
||||
var state = new GpuWorldState();
|
||||
var door = MakeServerEntity(id: 2, serverGuid: 0x7A9B4001u); // not marked persistent
|
||||
state.AppendLiveEntity(0xA9B40011u, door);
|
||||
|
||||
state.RemoveLandblock(0xA9B4FFFFu);
|
||||
|
||||
Assert.Empty(state.DrainRescued());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue