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

12 KiB
Raw Blame History

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.WriteLines 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):
    $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 startedteleport 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.