fix(streaming): #138 — re-hydrate server objects from the retained spawn table on reload
Doors/NPCs/portals vanished after a portal OUT of the 0x0007 dungeon back to Holtburg. Root cause confirmed via ACE + holtburger cross-reference: the dungeon collapse drops a landblock's render entities for FPS, and ACE will NOT re-broadcast objects whose guid is still in its per-player KnownObjects set (never cleared on a normal teleport — ACE relies on the client retaining its object table and culling stale objects itself). So nothing restored them on the way back. Retail-faithful fix: a real client keeps its weenie_object_table and re-renders the world from it (holtburger keeps the table across a teleport; only suspends physics bodies). acdream's _lastSpawnByGuid (the parsed CreateObject records — position + Setup + appearance) IS that table and survives the collapse (the collapse path never calls RemoveLiveEntityByServerGuid, the only thing that prunes it). On landblock (re)load, replay OnLiveEntitySpawnedLocked for retained spawns whose render entity is absent — independent of any ACE re-send. - LandblockEntityRehydrator: pure selection (landblock match; skip already-present, the player, and mesh-less spawns), unit-tested (7). - StreamingController: onLandblockLoaded callback after AddLandblock (Loaded = dungeon-exit expand) and AddEntitiesToExistingLandblock (Promoted = Far->Near). - GameWindow.RehydrateServerEntitiesForLandblock: present-gate keys on GpuWorldState (NOT _entitiesByServerGuid, which holds collapse orphans), replay under _datLock; the replay's own RemoveLiveEntityByServerGuid de-dup scrubs the orphan state. Corrects the handoff: ClientObjectTable is inventory-only (no world position/Setup) and cannot rebuild a render entity; _lastSpawnByGuid is the world-object table. Register row AP-48 (no retail 25s visibility cull). dotnet build + 1518 Core tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9445f53fe
commit
bf66fb4123
5 changed files with 311 additions and 3 deletions
|
|
@ -2295,7 +2295,11 @@ public sealed class GameWindow : IDisposable
|
|||
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
||||
_buildingRegistries.Remove(id & 0xFFFF0000u); // Phase A8 (key normalization fix 2026-05-28)
|
||||
_envCellRenderer?.RemoveLandblock(id); // Phase A8
|
||||
});
|
||||
},
|
||||
// #138: restore retained server objects when a landblock reloads
|
||||
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
|
||||
// objects it thinks we still know, so we re-project them ourselves.
|
||||
onLandblockLoaded: RehydrateServerEntitiesForLandblock);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
|
||||
|
|
@ -2681,6 +2685,77 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #138: re-hydrate retained server objects (doors, NPCs, chests, portals)
|
||||
/// into a landblock that just (re)loaded. Fired by
|
||||
/// <see cref="AcDream.App.Streaming.StreamingController"/> after
|
||||
/// <c>AddLandblock</c> / <c>AddEntitiesToExistingLandblock</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// The dungeon collapse (and Near→Far demote) drops a landblock's render
|
||||
/// entities for FPS but keeps the parsed spawns in <see cref="_lastSpawnByGuid"/>
|
||||
/// (our <c>weenie_object_table</c> for world objects). ACE never
|
||||
/// re-broadcasts objects it believes we still know — its per-player
|
||||
/// <c>KnownObjects</c> set is not cleared on a normal teleport (verified
|
||||
/// against <c>references/ACE</c> <c>ObjectMaint</c>; a real client keeps its
|
||||
/// table and re-renders from it, per <c>references/holtburger</c>). So on
|
||||
/// reload the render side would stay empty. We re-project the objects from
|
||||
/// our own retained table instead — independent of any server re-send.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Idempotent and cheap when nothing is missing: the selection skips guids
|
||||
/// already present in <see cref="_worldState"/> (initial login, or objects
|
||||
/// ACE did re-send), the player (owned by the persistent-entity rescue
|
||||
/// path), and spawns with no world mesh. The replay's own
|
||||
/// <see cref="RemoveLiveEntityByServerGuid"/> de-dup scrubs the state the
|
||||
/// collapse orphaned — the entity lingers in <see cref="_entitiesByServerGuid"/>
|
||||
/// after <c>RemoveLandblock</c> even though its render entity is gone.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void RehydrateServerEntitiesForLandblock(uint loadedLandblockId)
|
||||
{
|
||||
if (_lastSpawnByGuid.Count == 0) return;
|
||||
|
||||
// Server guids that already have a live render entity. The gate keys on
|
||||
// GpuWorldState (the render projection), NOT _entitiesByServerGuid, which
|
||||
// still holds collapse-orphaned entries whose render entity is gone.
|
||||
var present = new HashSet<uint>();
|
||||
foreach (var e in _worldState.Entities)
|
||||
if (e.ServerGuid != 0) present.Add(e.ServerGuid);
|
||||
|
||||
// Snapshot the retained spawns — the replay mutates _lastSpawnByGuid
|
||||
// (remove then re-add), so we must not iterate it live.
|
||||
var retained = new List<AcDream.App.Streaming.LandblockEntityRehydrator.RetainedSpawn>(
|
||||
_lastSpawnByGuid.Count);
|
||||
foreach (var kv in _lastSpawnByGuid)
|
||||
{
|
||||
var sp = kv.Value;
|
||||
bool hasWorldMesh = sp.Position is not null && sp.SetupTableId is not null;
|
||||
uint spawnLb = sp.Position is { } p ? p.LandblockId : 0u;
|
||||
retained.Add(new AcDream.App.Streaming.LandblockEntityRehydrator.RetainedSpawn(
|
||||
kv.Key, spawnLb, hasWorldMesh));
|
||||
}
|
||||
|
||||
var guids = AcDream.App.Streaming.LandblockEntityRehydrator.SelectGuidsToRehydrate(
|
||||
loadedLandblockId, retained, present, _playerServerGuid);
|
||||
if (guids.Count == 0) return;
|
||||
|
||||
// Replay through the normal live-spawn build under the dat lock (it reads
|
||||
// Setup/GfxObj/Surface dats). Each replay rebuilds the render entity into
|
||||
// the now-loaded landblock via AppendLiveEntity's hot path.
|
||||
lock (_datLock)
|
||||
{
|
||||
foreach (var guid in guids)
|
||||
if (_lastSpawnByGuid.TryGetValue(guid, out var spawn))
|
||||
OnLiveEntitySpawnedLocked(spawn);
|
||||
}
|
||||
|
||||
if (_options.DumpLiveSpawns)
|
||||
Console.WriteLine(
|
||||
$"live: re-hydrated {guids.Count} server object(s) into landblock 0x{loadedLandblockId:X8}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Door detection by server-sent name. Doors fail the generic
|
||||
/// multi-frame-idle gate at line 2692 (no idle cycle), so we register
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue