From b9445f53fe1191f7b0892dba9e2c697d9a9766d6 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 07:29:00 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20handoff=20for=20#138=20(entity=20re-del?= =?UTF-8?q?ivery=20after=20teleport)=20=E2=80=94=20next=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full orientation for a fresh session to fix #138: confirmed root (server objects unloaded on teleport-IN, not restored on return; ACE re-broadcast unreliable), DO-NOT-RETRY table (cache + render-cull eliminated), the ClientObjectTable re-hydrate fix direction with file:line pointers, the launch/probe/account setup, and the gotchas (re-broadcast latency, stale sessions, don't-kill-clients, entity.Id != ServerGuid). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-21-138-entity-redelivery-handoff.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/research/2026-06-21-138-entity-redelivery-handoff.md diff --git a/docs/research/2026-06-21-138-entity-redelivery-handoff.md b/docs/research/2026-06-21-138-entity-redelivery-handoff.md new file mode 100644 index 00000000..4dee2d3a --- /dev/null +++ b/docs/research/2026-06-21-138-entity-redelivery-handoff.md @@ -0,0 +1,178 @@ +# 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. + +--- + +## 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).