# Handoff — #138: server objects + own avatar don't come back after a teleport (entity RE-DELIVERY) **Date:** 2026-06-21 **Branch:** `claude/thirsty-goldberg-51bb9b` — committed, **NOT merged to main, NOT pushed** (user's call) **Milestone:** M1.5 "Indoor world feels right" **HEAD:** `c0b2cf2` (docs) on top of `a15bd3b` (the #145 fix). Tree is clean. --- > **✅ RESOLVED 2026-06-21 (`bf66fb4`+`0a5f91b`+`aa4a04d`, branch `claude/thirsty-goldberg-51bb9b`; > pending user visual gate).** The re-delivery diagnosis below was CONFIRMED by cross-reference > (ACE never clears `KnownObjects` on a teleport so it won't re-send known objects; retail/holtburger > keep the client object table and re-render from it). **BUT this handoff's recommended fix source > was WRONG:** `ClientObjectTable` is the INVENTORY model (`ClientObject` has no world > position/cell/Setup — see `src/AcDream.Core/Items/ClientObject.cs`) and cannot rebuild a render > entity. The real retained world-object table is **`GameWindow._lastSpawnByGuid`** (parsed > `WorldSession.EntitySpawn` records with position + Setup + appearance); it survives the collapse > and is what we re-project from. Shipped fix: `LandblockEntityRehydrator` + `StreamingController. > onLandblockLoaded` + `GameWindow.RehydrateServerEntitiesForLandblock`, plus a pending-bucket > persistent-entity rescue for the player-vanish. See `docs/ISSUES.md` #138 + register AP-48. The > rest of this doc is the (accurate) investigation trail; ignore its `ClientObjectTable` fix advice. ## What shipped this session | Commit | What | |---|---| | `a15bd3b` | **#145 FIXED + user-verified** — portals work repeatedly (run in / out / re-enter). Server-authoritative teleport placement. | | `c0b2cf2` | docs: #138 re-scoped to entity re-delivery + this handoff. | **#145 one-liner (context, DONE):** teleport-OUT of a dungeon mis-rooted the player into the SOURCE dungeon's coordinate frame (stale physics-landblock world-offset overlap after the streaming recenter), so acdream sent dungeon-framed positions and ACE rejected every move ("failed transition"). Fix = (1) drop the stale source center landblock from physics at the recenter so `Resolve` falls through to the server position (NO-LANDBLOCK verbatim), (2) place outdoor teleports immediately (streaming can't progress during a PortalSpace hold), (3) clear a dangling `CellGraph.CurrCell` when its landblock is removed (else the dungeon-streaming gate keeps streaming collapsed → only skybox). Detail: ISSUES `#145`, digest `claude-memory/project_physics_collision_digest.md` (2026-06-20 entry). --- ## THE NEXT WORK — #138: entity re-delivery across a teleport **Symptom (user):** after a portal OUT of the 0x0007 dungeon back to Holtburg, the server-spawned objects (doors, NPCs, portals, chests) don't appear; and after a couple of round-trips the player's OWN avatar stops rendering too. **Other clients (retail) DO see the acdream player** — so the server has correct player state; acdream's LOCAL world is incomplete. ### ✅ ROOT CAUSE (confirmed this session): RE-DELIVERY, not render, not cache acdream **unloads** the landblock's server-spawned objects on teleport-IN (the dungeon collapse unloads neighbours, including Holtburg → `GpuWorldState.RemoveLandblock`). On the way back, **nothing restores them**, and ACE does **not reliably re-broadcast** them. So they are simply absent from acdream's world. Decisive run (`notan`/`+Je`, walked around 20 s after returning): after teleport-OUT, `live: spawn` doors = **0**, `[ent] +` appends = **0**, `[ent-flat] server=1` (only the player), `[dyn] dyn=1`. The server delivered **zero** Holtburg objects on return. (An earlier `testaccount2` run got ~15 re-sent — but the user confirmed they *still* stayed missing — so re-delivery is **partial AND unreliable** across accounts/sessions, possibly worsened by the session's rapid-relaunch churn confusing ACE's per-player known-set.) ### 🚫 DO-NOT-RETRY (eliminated this session — don't re-investigate) | Hypothesis | Why it's dead | |---|---| | Tier-1 classification cache (`EntityClassificationCache`) | Re-created live entities get a **fresh monotonic `Id = _liveEntityIdCounter++`** (`GameWindow` ~:3251). The cache is keyed on `Id`, so it's **always a miss** for them. `ACDREAM_DISABLE_TIER1_CACHE=1` is irrelevant. | | Render-side cull (WALK / `EntityPassesVisibleCellGate` / `DrawDynamicsLast`) | The render path is **fine when entities are present**: at login `[dyn] rootOutdoor=True dyn=54 drawn=33`. The dynamics partition + `DrawDynamicsLast` draw every `ServerGuid!=0` entity (with `MeshRefs>0`). After teleport `dyn=1` only because the flat view itself has only 1 server entity (the player). | | Storage drop in `GpuWorldState` (AddLandblock record-replace, pending-merge) | The append/pending/flat-view path is correct; when objects ARE received they reach the flat view fine. The gap is that they're **never received** after the teleport. | ### 🔧 FIX DIRECTION (recommended) **Re-hydrate `GpuWorldState` from acdream's OWN retained object table on landblock reload**, instead of depending on an ACE re-broadcast that doesn't reliably arrive. - `ClientObjectTable` (`src/AcDream.Core/Items/ClientObjectTable.cs`, class :41; accessed as `GameWindow.Objects` :598; populated via `Ingest` on every CreateObject) is the retail `weenie_object_table` equivalent — it keeps **ALL** objects with their positions/appearance (see memory `project_object_item_model`: "CreateObject = canonical merge-upsert into ClientObjectTable"). It is NOT cleared on landblock unload. - So when `GpuWorldState.AddLandblock(lb)` runs (landblock streams in / reloads), walk `ClientObjectTable` for objects whose position falls in `lb` and re-build their render entities (the same `OnLiveEntitySpawnedLocked` mesh-ref build at `GameWindow` ~:3161-3215), appending via `AppendLiveEntity`. That makes re-delivery independent of ACE. - **Alternative** (simpler but heavier on memory): treat all in-range server objects as PERSISTENT across the collapse (like the player — `GpuWorldState.MarkPersistent` :242 + the rescue/re-inject at `GameWindow` ~:7419-7427), so they survive the unload entirely. The collapse exists for FPS (don't keep the ~129 ocean-grid neighbour dungeons' objects), so this must be scoped to the CURRENT landblock's objects only. - **Cross-check first** (don't guess the protocol): `references/holtburger` (`client/messages.rs`, the post-teleport handlers + object visibility) and `references/ACE` (`Source/ACE.Server/...` ObjMaint / landblock visibility / what triggers a re-CreateObject on a landblock change). Question to answer: **does retail's server release + re-send objects on a teleport landblock change, and does the client need to signal anything?** If ACE *should* re-send and isn't, the bug may be acdream not triggering ACE's awareness update — but the client-side re-hydrate above is robust regardless and is the retail-faithful "client keeps its object table" model. ### Key file:line pointers - **Entity storage / lifecycle:** `src/AcDream.App/Streaming/GpuWorldState.cs` — `AppendLiveEntity` :413 (loaded vs `_pendingByLandblock`), `AddLandblock` :199 (pending-merge :205-215 — **the re-hydrate hook goes here**), `RemoveLandblock` :285 (unload; rescue of `_persistentGuids` :294-302), `RemoveEntitiesFromLandblock` :464 (near→far demote; `_onLandblockUnloaded` :495), `DrainRescued` :331, `RelocateEntity` :254, flat view `RebuildFlatView` :550 / `Entities` :93, `LandblockEntries` :139. - **Live spawn → WorldEntity build:** `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveEntitySpawnedLocked` :2696 (dedup `RemoveLiveEntityByServerGuid` :2711; MeshRef build :3161-3215 — drops the entity if 0 mesh refs :3208; `Id = _liveEntityIdCounter++` :3251; `AppendLiveEntity` call :3304), rescued re-inject :7419-7427. - **Object table (the fix's source):** `src/AcDream.Core/Items/ClientObjectTable.cs`, `src/AcDream.Core/Items/ClientObject.cs` (`Ingest` merge-upsert). - **Render path (CONFIRMED FINE — for reference only):** `RetailPViewRenderer.DrawInside` :59 → `InteriorEntityPartition.Partition` (`src/AcDream.App/Rendering/InteriorEntityPartition.cs` :40 — `ServerGuid!=0 && MeshRefs>0` → `Dynamics` :51-58) → `RetailPViewRenderer.DrawDynamicsLast` :680 → `WbDrawDispatcher` (`WalkEntitiesInto` :657, `EntityPassesVisibleCellGate` :2227). `EntitySpawnAdapter` (`OnCreate` :100 / `OnRemove` :176), `ObjectMeshManager` refcount/LRU (`IncrementRefCount` :333, `DecrementRefCount` :368 — moves to LRU, doesn't free). ### Side-finding (separate minor bug, NOT #138 — fix later WITH verification) The Tier-1 cache has a **demote-vs-unload invalidation asymmetry**: `RemoveEntitiesFromLandblock` fires `_onLandblockUnloaded` (`GpuWorldState` :495) but `RemoveLandblock` does NOT — violating the cache's documented "demote OR unload" intent (`EntityClassificationCache.cs` docstring + the `GpuWorldState._onLandblockUnloaded` comment). For LIVE entities it's harmless (fresh Ids → always a miss), but for DAT-hydrated scenery (deterministic Ids that can recur on landblock reload) it could serve stale batches. One-line fix (add the `_onLandblockUnloaded?.Invoke((lbId & 0xFFFF0000)|0xFFFF)` to `RemoveLandblock`) but needs its own repro + verification; was tried this session and reverted because it does NOT fix #138. --- ## Launch / probes / accounts Canonical launch (PowerShell — the apostrophe in "Asheron's Call" breaks bash): ```powershell $env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" $env:ACDREAM_LIVE = "1" $env:ACDREAM_TEST_HOST = "127.0.0.1"; $env:ACDREAM_TEST_PORT = "9000" $env:ACDREAM_TEST_USER = "notan"; $env:ACDREAM_TEST_PASS = 'MittSnus81!' # single-quote the '!' dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object launch.log ``` **Accounts / characters used (all spawn near Holtburg, reach the 0x0007 Town Network dungeon):** - `notan` / `MittSnus81!` → **`+Je`** (guid `0x50000001`). Freshest; what the user prefers now. - `testaccount` / `testpassword` → `+Acdream` (guid `0x5000000A`). - `testaccount2` / `testpassword2` → `Horan` (guid `0x5000000B`). **Existing diagnostic env vars (already in the build):** - `ACDREAM_DUMP_LIVE_SPAWNS=1` → `live: spawn guid=… name="Door"/"Chest"/… @0xLLLLcccc` per CreateObject **received**. The smoking gun for "did the server re-deliver?" (0 lines after a teleport = not re-delivered). - `ACDREAM_DISABLE_TIER1_CACHE=1` (A/B; irrelevant to #138 per above). - `ACDREAM_DUMP_ENTITY=` → `[dump-entity] WALK-REJECT/DRAW` — matches `entity.Id` (the **counter**, not the guid — hard to target for re-created entities). **Throwaway probes used this session (re-add if needed — were reverted):** - `[ent-flat] server=N total=M player=Y/n lbs=K` — in `GpuWorldState.RebuildFlatView`: count of `ServerGuid!=0` in the rendered flat view, logged on change. (Player = `_persistentGuids`.) - `[ent] +0xGUID -> 0xLB loaded|PENDING` / `[ent] rmlb …` — in `AppendLiveEntity` / `RemoveLandblock`, for `ServerGuid!=0`. - `[dyn] rootOutdoor=B dyn=N skip-outdoor=K cone-cull=C drawn=D` — in `RetailPViewRenderer.DrawDynamicsLast` (throttled 1/60 frames). --- ## ⚠️ GOTCHAS (cost real time this session) 1. **Re-broadcast latency masks the layer.** ACE re-sends objects over **several seconds** after a teleport (if at all). Closing the window ~1 s after returning shows "no objects" REGARDLESS of the bug. Always **wait/walk ~15-20 s** after returning before judging. This caused ~4 inconclusive runs. The reliable signal is the `live: spawn` count, not eyeballing. 2. **Stale-session login failures.** Rapid relaunches leave ACE holding the prior session → `live: session failed: CharacterList not received` (exit 29) on the next launch. No admin kick available; wait it out (~minutes) or switch accounts. A *lingering client process* also holds the slot. 3. **The user does NOT want clients killed/closed by the agent.** Do not `Stop-Process` / `CloseMainWindow` acdream. Just `dotnet run` when asked; read logs (reading ≠ killing). 4. **`entity.Id != entity.ServerGuid`.** `Id` is a per-session monotonic counter (`_liveEntityIdCounter++`); `ServerGuid` is the wire guid (e.g. a door `0x7A9B40xx`). Filter probes by `ServerGuid`. 5. **Visual confirmation requires the user** — they run the in-world actions; the agent reads the logs. This is the one genuine stop point. ## Pointers - ISSUES `#138` (full re-scope writeup + acceptance), `#145` (DONE), `#137` (dungeon collision, OPEN). - memory `project_object_item_model` (ClientObjectTable model), `project_render_pipeline_digest.md` (render SSOT — confirms render is fine), `project_physics_collision_digest.md` (#145 detail). - references: `holtburger` (client behaviour — what a real client sends/expects post-teleport), `ACE` (server expectations — object visibility on landblock change).