191 lines
13 KiB
Markdown
191 lines
13 KiB
Markdown
# 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=<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).
|