# 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 3–5 (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 1–3, 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.