acdream/docs/research/2026-06-21-138-entity-redelivery-handoff.md
Erik b07825cd24 docs(research): #138 handoff — RESOLVED banner + correct the re-hydrate source (not ClientObjectTable)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:09:36 +02:00

13 KiB

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.

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.csAppendLiveEntity :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>0Dynamics :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):

$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 / testpassword2Horan (guid 0x5000000B).

Existing diagnostic env vars (already in the build):

  • ACDREAM_DUMP_LIVE_SPAWNS=1live: 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=<hex,hex>[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).