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, branchclaude/thirsty-goldberg-51bb9b; pending user visual gate). The re-delivery diagnosis below was CONFIRMED by cross-reference (ACE never clearsKnownObjectson 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:ClientObjectTableis the INVENTORY model (ClientObjecthas no world position/cell/Setup — seesrc/AcDream.Core/Items/ClientObject.cs) and cannot rebuild a render entity. The real retained world-object table isGameWindow._lastSpawnByGuid(parsedWorldSession.EntitySpawnrecords 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. Seedocs/ISSUES.md#138 + register AP-48. The rest of this doc is the (accurate) investigation trail; ignore itsClientObjectTablefix 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 asGameWindow.Objects:598; populated viaIngeston every CreateObject) is the retailweenie_object_tableequivalent — it keeps ALL objects with their positions/appearance (see memoryproject_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), walkClientObjectTablefor objects whose position falls inlband re-build their render entities (the sameOnLiveEntitySpawnedLockedmesh-ref build atGameWindow~:3161-3215), appending viaAppendLiveEntity. 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 atGameWindow~: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) andreferences/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 viewRebuildFlatView:550 /Entities:93,LandblockEntries:139. - Live spawn → WorldEntity build:
src/AcDream.App/Rendering/GameWindow.csOnLiveEntitySpawnedLocked:2696 (dedupRemoveLiveEntityByServerGuid:2711; MeshRef build :3161-3215 — drops the entity if 0 mesh refs :3208;Id = _liveEntityIdCounter++:3251;AppendLiveEntitycall :3304), rescued re-inject :7419-7427. - Object table (the fix's source):
src/AcDream.Core/Items/ClientObjectTable.cs,src/AcDream.Core/Items/ClientObject.cs(Ingestmerge-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),ObjectMeshManagerrefcount/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(guid0x50000001). Freshest; what the user prefers now.testaccount/testpassword→+Acdream(guid0x5000000A).testaccount2/testpassword2→Horan(guid0x5000000B).
Existing diagnostic env vars (already in the build):
ACDREAM_DUMP_LIVE_SPAWNS=1→live: spawn guid=… name="Door"/"Chest"/… @0xLLLLccccper 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— matchesentity.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— inGpuWorldState.RebuildFlatView: count ofServerGuid!=0in the rendered flat view, logged on change. (Player =_persistentGuids.)[ent] +0xGUID -> 0xLB loaded|PENDING/[ent] rmlb …— inAppendLiveEntity/RemoveLandblock, forServerGuid!=0.[dyn] rootOutdoor=B dyn=N skip-outdoor=K cone-cull=C drawn=D— inRetailPViewRenderer.DrawDynamicsLast(throttled 1/60 frames).
⚠️ GOTCHAS (cost real time this session)
- 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: spawncount, not eyeballing. - 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. - The user does NOT want clients killed/closed by the agent. Do not
Stop-Process/CloseMainWindowacdream. Justdotnet runwhen asked; read logs (reading ≠ killing). entity.Id != entity.ServerGuid.Idis a per-session monotonic counter (_liveEntityIdCounter++);ServerGuidis the wire guid (e.g. a door0x7A9B40xx). Filter probes byServerGuid.- 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).