acdream/docs/research/2026-06-21-teleport-foundation-handoff.md
Erik f8cda7e86d docs(teleport): foundation-investigation handoff for a fresh-look session
Captures the full teleport-flow journey (Slice 1 kept, Slice 2 hold built +
reverted), the root-cause findings (the IsLandblockLoaded key bug + the real
foundation problem: destination doesn't stream fast/complete during teleport,
likely _datLock starvation from the CreateObject flood), the open foundation
issues (#138 slow/incomplete streaming, lost-collision-after-teleport, FPS
leak Work-C, the PortalSpace freeze-vs-run-through question), and the clean
baseline (dd2eb8b). Written as evidence + open questions, not conclusions, so
a fresh session can re-examine the whole approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:00:23 +02:00

176 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff — teleport foundation (fresh-look investigation) — 2026-06-21
**Branch:** `claude/thirsty-goldberg-51bb9b` — committed, NOT merged.
**Clean baseline commit:** `dd2eb8b` (the Slice 2 revert). Build + tests green here.
**Account for repro:** `notan` / `MittSnus81!``+Je`. The character is currently left at a far
outdoor town (landblock ~125,100 = `0x7D64`); portal via the Town Network hub (`0x0007`, an indoor cell)
to reach far towns.
> **Read this with fresh eyes.** The previous session built a "hold the player until the destination
> loads" teleport feature, hit a wall, and reverted it. This doc gives you the **evidence and the open
> questions** — NOT a prescription. You are explicitly invited to question every assumption below,
> including whether a "hold" is the right model at all. Investigate first (read-only), report, then
> brainstorm the fix with the user. **No band-aids** (the user's words: "feels shaky and bandaid").
---
## TL;DR
Teleporting in acdream feels bad: **long transitions, the character STOPS at the portal instead of
running through, FPS sags after portaling, and a building lost its collision after a death→lifestone
teleport.** The previous session traced the "long transition" to a real root: **the destination
landblock does not stream in fast (or completely) during/around a teleport** (the #138 streaming gap),
likely amplified by **`_datLock` contention** from ACE's CreateObject flood on arrival. A "hold until
loaded" gate just made every teleport wait ~10 s for a load that wasn't happening — so it was reverted.
**The real work is the foundation: make teleport-destination streaming fast AND complete, and fix the
post-teleport FPS leak.** Then (and only then) a retail-style visual cover is polish, not a crutch.
---
## What was built, and what was reverted
The original goal was **Work item B: the retail teleport flow** (fade → portal-tunnel → hold-until-loaded
→ place; covers login/logout/death/portal). Design + plan are committed and still valid as *aspiration*:
- Spec: `docs/superpowers/specs/2026-06-21-retail-teleport-flow-design.md`
- Plan: `docs/superpowers/plans/2026-06-21-retail-teleport-flow.md` (22 TDD tasks, 5 slices)
- Prior research: `docs/research/2026-06-21-teleport-issues-handoff.md` (the TAS state-machine decomp).
Shipped, then status:
- **Slice 1 — `TeleportAnimSequencer`** (pure 7-state TAS machine, `src/AcDream.Core/World/`): **KEPT.**
Dormant, unwired, fully unit-tested (29 tests). Harmless. Reusable later.
- **Slice 2 — the readiness-gate hold** (outdoor teleport holds until the physics landblock is loaded):
**REVERTED** (`dd2eb8b`, reverting `ad8c24e..c880973` back to `00ef47e`). Outdoor teleports now place
**immediately** again (fast). See "Why reverted" below.
- **Slices 35 (the TAS animation/controller, login/logout/death unification): PARKED** (never started).
### Why Slice 2 was reverted (the key learning)
Slice 2 gated outdoor arrival on "is the destination physics landblock resident?" and held otherwise.
Live testing showed **every outdoor teleport froze ~10 s then force-placed**, because the destination
landblock **never became resident during the hold** (`lbs=0` the whole time). The hold was band-aiding a
foundation problem (slow/incomplete streaming) instead of fixing it — and it didn't even prevent the
#145 edge cascade it was designed for (it force-snapped onto NO-LANDBLOCK after the timeout regardless).
---
## Root-cause findings so far (verify, don't trust)
A 3-agent read-only investigation (workflow `wf_8b67a9d1-35c`, all high-confidence) + manual
source-verification found **two distinct problems** behind the 10 s freeze:
### 1. A real (now-reverted) bug: `IsLandblockLoaded` key mismatch
The Slice 2 gate queried `IsLandblockLoaded(destCell & 0xFFFF0000)` (e.g. `0x7D640000`), but streaming
stores landblocks under the `EncodeLandblockId` form — low 16 bits = `0xFFFF` (e.g. `0x7D64FFFF`)
(`src/AcDream.App/Streaming/StreamingRegion.cs:98-99`). A raw `ContainsKey` never matched → the gate was
permanently `NotReady`. This was **fixed** (`c880973`, normalize the key + the regression test the
original missed) then **reverted along with the rest of Slice 2**. *If a future hold is reintroduced, the
predicate must normalize the key (the fix is preserved in history at `c880973`).*
### 2. The actual foundation problem: the destination doesn't stream in fast/complete
Even with the key bug fixed, `lbs=0` (zero landblocks resident) for the entire hold — the destination
genuinely was not loaded. Evidence + leads:
- The streaming machinery is *commanded* correctly: on a dungeon-OUT teleport the gate logs
`streaming: dungeon EXIT-expand -> (Lx,Ly)` and the observer is correctly pinned to the destination
during the hold (`src/AcDream.App/Rendering/GameWindow.cs` ~7441-7459 PortalSpace observer-pin;
`DungeonStreamingGate.cs:44-45` returns `InsideDungeon=false` for a teleport hold;
`StreamingController.cs` ~130-163 Tick dispatch, ~279-297 `ExitDungeonExpand`, ~161-163 `DrainAndApply`
runs unconditionally). So the *command* path is not obviously gated on `InWorld`.
- **Yet nothing loads during the hold, and the landblock appears the instant the player flips to
`InWorld`.** The leading hypothesis (MEDIUM confidence — NOT proven) is **`_datLock` starvation**: ACE
floods CreateObjects on arrival; `WorldSession.Tick` drains them in a tight loop
(`src/AcDream.Core.Net/WorldSession.cs` ~598-607, no yield), each under `lock(_datLock)`
(`GameWindow.cs` ~2679-2682), and the streaming worker also needs `_datLock` to build a landblock
(`BuildLandblockForStreaming` — confirm the exact site; the investigation cited a `_datLock` acquire on
the build path). So the render-thread CreateObject flood may block the background streamer for the whole
arrival window. **This is the #1 thing to confirm or refute.**
- `AddLandblock` registers terrain + cells + portals **atomically** (`PhysicsEngine.cs` ~66-74), and the
spatial lookups (`SampleTerrainZ`, `TryGetLandblockContext`) find landblocks by world-XY **bounds**, not
by dictionary key — which is why a force-placed player grounds correctly the instant the LB appears.
### 3. The lost-collision symptom (death→lifestone → building has no collision)
Not yet root-caused. Hypothesis: the destination loaded **incompletely** — terrain present but the
building/cell **collision** (`ShadowObjects` / the per-cell shadow list) not registered. See
`memory/feedback_retail_per_cell_shadow_list.md` (retail collision = per-cell list with portal-aware
registration). Likely the same root as #2 (incomplete async load), but **verify independently** — it may
be a distinct registration-order bug.
### 4. FPS sag after portaling (Work item C)
Separate known issue (a cumulative leak). Candidates from the prior research: streaming/entity state not
freed on teleport round-trips, `_lastSpawnByGuid` / `GpuWorldState` accumulation, dungeon collapse/expand
not cleaning up. The `_datLock` contention also contributes to per-frame hitching during the flood.
**Note:** `ACDREAM_PROBE_RESOLVE=1` is itself a massive FPS killer (it `Console.WriteLine`s every physics
resolve — a 561k-line log). Do NOT measure FPS with it on; it confounds everything.
---
## The investigation to run (the new session's job)
Root-cause, read-only, then report + brainstorm. Core questions:
1. **Why is the teleport destination not resident fast?** Confirm/refute the `_datLock` starvation
hypothesis: instrument (lightly) the streamer worker's load completions and the `_datLock` hold time
during an arrival, and see whether the worker is blocked by the CreateObject flood. Is the async
load→apply pipeline gated, starved, or just slow? Does the dungeon collapse→EXIT-expand transition add
latency?
2. **Why does collision get dropped** after a teleport (the lost-building-collision)? Trace where cell /
ShadowObject collision is registered during streaming and what order vs terrain; is it skipped or
raced on a teleport arrival?
3. **What leaks** (Work item C) across teleport round-trips? Capture landblock + entity counts across N
teleports; find what grows unbounded.
4. **What does retail actually do?** The decomp (`docs/research/named-retail/`) + the prior TAS research
show retail plays a tunnel animation that *covers a synchronous-feeling load*, gated on
`position_update_complete` (the DDD interrogation). Retail's load is fast within the tunnel. So the
question isn't "hold vs no-hold" in the abstract — it's **"how do we make the load fast + complete,"**
after which either a short hold-with-cover OR place-immediately becomes viable.
## Open DESIGN questions to reconsider with fresh eyes (don't assume the prior answers)
- **Hold vs place-immediately.** The reverted baseline places immediately (fast, but risks pop-in /
the #145 edge cascade on a not-yet-streamed edge arrival). Retail holds behind a tunnel anim but loads
fast. Which model fits once streaming is fast? (AC2D, a reference, sends movement keys + trusts server
Z and does no client-side hold — worth weighing.)
- **The `PortalSpace` input-freeze.** Today entering a teleport freezes input (`OnTeleportStarted` sets
`PlayerState.PortalSpace`), so the **character STOPS at the portal** instead of running through. The
user explicitly wants "run through the portal." Is freezing input on portal-enter even correct? What
does retail do (keep momentum? freeze?)? This is independent of streaming and may be its own fix.
- **The #145 edge cascade.** With place-immediately restored, a teleport that arrives near a landblock
EDGE onto an unstreamed neighbour can still march the cell label + free-fall Z (the original #145
residual). The hold never actually fixed this. The real fix is fast/complete streaming so the arrival
lands on a loaded world; failing that, the cell-relative physics frame (#145 Slices 13, already
shipped) limits it.
---
## Apparatus / how to reproduce + diagnose
- **Launch** (PowerShell; client lifecycle is the USER's — launch with plain `dotnet run`, do NOT
pre-close clients; if a rebuild is locked by a running client, ASK):
```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!"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object launch.log
```
Build green FIRST (`dotnet build src\AcDream.App\AcDream.App.csproj -c Debug`); launch uses `--no-build`.
- **Repro the bad teleport:** in-world, portal via the hub (`0x0007`) to a far town, or recall to
lifestone. Watch the `live: teleport started` → `teleport complete` gap and `[snap] ... NO-LANDBLOCK
(lbs=0)`.
- **Diagnostics:** `ACDREAM_PROBE_RESOLVE=1` exists but is FPS-toxic + confounding — prefer a *new, light*
probe (e.g. log streamer load-completions + `_datLock` hold-time during an arrival). The prior apparatus
logs (`launch-gate-retest*.log`, `launch-clean-noprobe*.log`) are in the worktree root but transient —
the evidence above is the durable summary.
- **It's a Debug build** — FPS is not representative; a `Release` build is far faster. Build Release if you
want a real frame-rate read.
## Clean baseline — what NOT to touch
- `dd2eb8b` is the clean baseline: outdoor teleports place immediately, Slice 1 sequencer dormant.
- Do NOT re-introduce the Slice 2 hold without fixing the streaming foundation first (that's the band-aid
we removed).
- Frozen/shipped phases per the milestones doc remain off-limits.
## Key references
- Render/physics digests: `claude-memory/project_render_pipeline_digest.md`,
`claude-memory/project_physics_collision_digest.md` (DO-NOT-RETRY tables).
- Streaming refs: `memory/reference_two_tier_streaming.md`, `memory/reference_indoor_cell_tracking.md`.
- `memory/feedback_apparatus_for_physics_bugs.md`, `memory/feedback_retail_per_cell_shadow_list.md`,
`memory/feedback_verify_subagent_claims_against_source.md` (read cited code yourself; agents fabricate).
- ISSUES: #138 (teleport-OUT EXPAND gap), #145 (edge cascade), Work item C (FPS leak) — update
`docs/ISSUES.md` with the new symptoms (lost-collision-after-teleport; char-stops-at-portal) as part of
the investigation.