diff --git a/docs/ISSUES.md b/docs/ISSUES.md index efe6cf7e..a776a181 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -195,7 +195,7 @@ Tests: `DungeonStreamingGateTests` (4), `TeleportArrivalRulesTests` (4). Registe ## #138 — Teleport OUT of a dungeon loads the outdoor world incompletely + position desync -**Status:** OPEN +**Status:** FIX SHIPPED 2026-06-21 (worktree `claude/thirsty-goldberg-51bb9b`; pending user visual gate). Server-object re-delivery root cause confirmed via ACE + holtburger cross-reference; fixed by client-side re-hydrate from the retained spawn table. Player-vanish (B) addressed by a pending-bucket rescue fix (candidate — needs visual confirm). **Severity:** MEDIUM (breaks the dungeon→outdoor transition; collision + visuals wrong after exit) **Filed:** 2026-06-14 **Component:** streaming — dungeon collapse↔expand (the #133/#135 collapse) + teleport-arrival @@ -232,7 +232,15 @@ Both are the **entity-lifecycle/render path across a teleport**, NOT the streami - **The ACTUAL cause: re-delivery is unreliable / absent.** `notan/+Je` walk-around run: after teleport-out, `live:spawn` doors = **0**, `[ent] +` door appends = **0**, `[ent-flat] server=1` (only the player), `[dyn] dyn=1` — the server delivered **ZERO** Holtburg objects on return; they never reach acdream. (An earlier `testaccount2` run got ~15 re-sent, but the user confirmed they stayed missing then too — so re-delivery is partial AND unreliable across accounts/sessions.) "Other clients see +Je" → the server has correct player state; acdream's LOCAL world is simply missing the objects. The 2026-06-20 "NOT a re-broadcast gap" claim above was WRONG — it IS (intermittently) a re-broadcast gap. - **Mechanism:** acdream UNLOADS the landblock's server-spawned objects on teleport-IN (the dungeon collapse unloads neighbours, including Holtburg), and on the way back NOTHING restores them; ACE does not reliably re-broadcast them (known-set/awareness desync, likely worsened by the rapid relaunch churn). -**Fix direction (NEXT):** restore the landblock's server objects on reload from acdream's OWN retained data model — the `ClientObjectTable` keeps ALL objects (see `project_object_item_model`: "CreateObject = merge-upsert into ClientObjectTable") — i.e. re-hydrate `GpuWorldState` from `ClientObjectTable` on `AddLandblock`, rather than depend on an ACE re-broadcast that doesn't reliably arrive. (Alternative: treat all in-range server objects as persistent across the collapse, like the player.) Cross-check the teleport object-visibility handshake against `references/holtburger` + `references/ACE` (does the client need to signal it dropped the objects? does ACE release on landblock change?). **Best started fresh — this is an entity-lifecycle/protocol change, not a render fix.** +**FIX SHIPPED 2026-06-21 — client-side re-hydrate from the retained spawn table (re-projection, not ACE re-send):** + +**⚠️ Handoff correction:** the 2026-06-21 handoff said to re-hydrate from `ClientObjectTable` ("keeps ALL objects with their positions"). That is WRONG — `ClientObject` (`src/AcDream.Core/Items/ClientObject.cs`) is the INVENTORY data model (container/slot/equip/icon/value/stack); it carries **no world position, no cell id, no Setup/PhysicsDesc/mesh**. It cannot build a render entity. The real retained world-object table is **`GameWindow._lastSpawnByGuid`** (`Dictionary`) — the parsed `CreateObject` records, carrying Position + Setup + MotionTable + AnimPartChanges + palette. It is pruned ONLY by a server `DeleteObject` or a spawn de-dup, so it **survives the dungeon collapse** (verified: the collapse path `StreamingController.Tick → GpuWorldState.RemoveLandblock` never calls `RemoveLiveEntityByServerGuid`, the only thing that touches `_lastSpawnByGuid`). + +**Cross-reference confirmation (this is retail-faithful, not a workaround):** ACE (`references/ACE/Source/ACE.Server/Physics/Common/ObjectMaint.cs`) **never clears a player's `KnownObjects` set on a normal teleport** (`teleport_visibility_fix` is off by default + flagged non-retail), so it will NOT re-send objects it thinks we still have — it relies on the client retaining its table and doing its own 25 s/384 m visibility cull. holtburger (`references/holtburger/.../handlers/player.rs`) **keeps its entire object table across a teleport** (only suspends physics bodies) and re-projects its displayed world from that table; stale far objects self-evict after a 25 s timeout. So "client keeps the object table and re-renders from it" IS retail behavior. acdream's render entities are the projection; the collapse drops the projection for FPS (AP-36), and re-hydrate rebuilds it on reload. + +**Implementation (A — server objects):** new `StreamingController` `onLandblockLoaded` callback fires after `AddLandblock` (Loaded path = dungeon-exit expand) and `AddEntitiesToExistingLandblock` (Promoted = Far→Near). `GameWindow.RehydrateServerEntitiesForLandblock` builds the set of server guids already present in `GpuWorldState`, snapshots `_lastSpawnByGuid`, and — via the pure `LandblockEntityRehydrator.SelectGuidsToRehydrate` (selects spawns in the loaded landblock that have a world mesh, are not the player, and are NOT already present) — replays `OnLiveEntitySpawnedLocked` for each missing guid under the dat lock. The replay's own `RemoveLiveEntityByServerGuid` de-dup scrubs the state the collapse orphaned (the entity lingers in `_entitiesByServerGuid` after `RemoveLandblock` even though its render entity is gone — so the present-gate keys on `GpuWorldState`, not that map). Idempotent + no double-build on initial login (entities already present → skipped). Commit: `LandblockEntityRehydrator.cs` + `StreamingController.cs` + `GameWindow.cs`; tests `LandblockEntityRehydratorTests` (7). Register row **AP-48** (no 25 s cull → a re-hydrate may restore an object the server silently dropped — port holtburger's cull to close). + +**Implementation (B — player vanish, candidate):** `GpuWorldState.RemoveLandblock` rescued persistent entities only from `_loaded`, silently dropping a persistent entity sitting in the **pending bucket** (the player is re-injected via `AppendLiveEntity` every frame; right after a teleport its landblock hasn't streamed yet → pending; if that landblock is then unloaded mid-churn the player was dropped → "vanishes after a couple round-trips"). Fix: rescue persistent pending entries too. Tests `RemoveLandblock_RescuesPersistentEntity_FromPendingBucket` (+ negative). This is a provable correctness fix for the "persistent ⇒ survives unload" invariant; it is the leading candidate for symptom B but needs user re-verification to confirm it is the complete cause. **Acceptance:** portal out of the 0x0007 dungeon → full outdoor world streams (trees/scenery present), **server objects (doors/NPCs/portals) render**, **own avatar renders across repeated round-trips**, collision works, position tracks (no avatar-vs-camera desync).