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>
86 lines
4.3 KiB
C#
86 lines
4.3 KiB
C#
using System.Collections.Generic;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// Decides which retained server-object spawns to re-hydrate (rebuild render
|
|
/// entities for) when a landblock (re)loads. The pure selection half of the
|
|
/// #138 fix; <c>GameWindow</c> owns the replay half (it alone can rebuild a
|
|
/// mesh from a <c>CreateObject</c>).
|
|
///
|
|
/// <para>
|
|
/// <b>Why this exists (retail/ACE model).</b> A real AC client keeps its
|
|
/// <c>weenie_object_table</c> across a teleport and re-projects its rendered
|
|
/// world from that table; the server does NOT re-broadcast objects it believes
|
|
/// the client already knows (ACE's per-player <c>KnownObjects</c> set is never
|
|
/// cleared on a normal teleport — ACE relies on the client retaining the
|
|
/// table, cross-checked against <c>references/holtburger</c> +
|
|
/// <c>references/ACE/Source/ACE.Server/Physics/Common/ObjectMaint.cs</c>).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// acdream's dungeon-collapse (and Near→Far demote) drops a landblock's
|
|
/// RENDER entities for FPS but retains the parsed spawns
|
|
/// (<c>GameWindow._lastSpawnByGuid</c> — the table is only pruned by an
|
|
/// explicit server <c>DeleteObject</c> or a spawn de-dup). On reload, ACE will
|
|
/// not re-send the objects it still thinks we have, so the render side stays
|
|
/// empty — the #138 symptom. This selects the retained spawns whose render
|
|
/// entity is currently absent so <c>GameWindow</c> can replay them, making
|
|
/// re-delivery independent of the server.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class LandblockEntityRehydrator
|
|
{
|
|
/// <summary>
|
|
/// A retained spawn reduced to just the fields the selection needs.
|
|
/// <paramref name="HasWorldMesh"/> is true only when the spawn carries both
|
|
/// a world position AND a Setup id — i.e. it would actually build a visible
|
|
/// render entity (inventory items and setup-less spawns produce none and
|
|
/// are skipped, matching <c>OnLiveEntitySpawnedLocked</c>'s own guard).
|
|
/// </summary>
|
|
public readonly record struct RetainedSpawn(uint Guid, uint SpawnLandblockId, bool HasWorldMesh);
|
|
|
|
/// <summary>
|
|
/// Canonical landblock id (low 16 bits forced to <c>0xFFFF</c>) — the same
|
|
/// keying <see cref="GpuWorldState.AppendLiveEntity"/> uses, so a
|
|
/// cell-resolved spawn id (<c>0xAAAA00CC</c>) and a streamed landblock id
|
|
/// (<c>0xAAAAFFFF</c>) compare equal when they name the same landblock.
|
|
/// </summary>
|
|
public static uint Canonicalize(uint landblockId) => (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
|
|
|
/// <summary>
|
|
/// Select the guids whose retained spawn should be replayed for the
|
|
/// just-loaded <paramref name="loadedLandblockId"/>.
|
|
/// </summary>
|
|
/// <param name="loadedLandblockId">The landblock that just (re)loaded.</param>
|
|
/// <param name="retainedSpawns">Snapshot of the retained world-object spawns.</param>
|
|
/// <param name="presentServerGuids">
|
|
/// Server guids that already have a live render entity in
|
|
/// <see cref="GpuWorldState"/>. A guid here was either never dropped or was
|
|
/// already re-delivered by the server, so replaying it would be redundant.
|
|
/// </param>
|
|
/// <param name="playerServerGuid">
|
|
/// The local player's guid — never re-hydrated here; it is owned by the
|
|
/// persistent-entity rescue path (<see cref="GpuWorldState.MarkPersistent"/>
|
|
/// + <c>DrainRescued</c>), which preserves the player's live pose/position
|
|
/// rather than resetting it to the spawn snapshot.
|
|
/// </param>
|
|
public static List<uint> SelectGuidsToRehydrate(
|
|
uint loadedLandblockId,
|
|
IReadOnlyCollection<RetainedSpawn> retainedSpawns,
|
|
IReadOnlySet<uint> presentServerGuids,
|
|
uint playerServerGuid)
|
|
{
|
|
uint loadedCanonical = Canonicalize(loadedLandblockId);
|
|
var result = new List<uint>();
|
|
foreach (var s in retainedSpawns)
|
|
{
|
|
if (!s.HasWorldMesh) continue; // no visible entity to rebuild
|
|
if (s.Guid == playerServerGuid) continue; // player: persistent-rescue path owns it
|
|
if (Canonicalize(s.SpawnLandblockId) != loadedCanonical) continue; // different landblock
|
|
if (presentServerGuids.Contains(s.Guid)) continue; // already rendered
|
|
result.Add(s.Guid);
|
|
}
|
|
return result;
|
|
}
|
|
}
|