acdream/docs/research/2026-06-21-teleport-issues-handoff.md
2026-06-21 18:24:08 +02:00

10 KiB
Raw Blame History

Handoff — teleport issues cluster (2026-06-21)

Branch: claude/thirsty-goldberg-51bb9b — committed, NOT merged ("merge later"). Account: notan / MittSnus81!+Je. Portal via the Town Network hub (0x0007) to far towns.


What shipped this session (context)

#145 cell-relative physics frame (Option B), Slices 13 + 7 (438bb68403a338). The carried-anchor fix closes the far-town cascade for a STREAMED-terrain arrival — live-verified across ~10 far-town landblocks over 2 sessions, zero march. Design + plan: docs/superpowers/specs/2026-06-21-145-cell-relative-physics-frame-design.md + the matching plan. Adversarial review passed (2 flagged issues were verified false positives). Slices 46 (contact-plane / walkable cell-relative + _liveCenter fully render-only — invisible architectural completeness) remain.


Work item A — #145 RESIDUAL (reopened): unstreamed-arrival-near-edge cascade

Trigger: a teleport that arrives onto a NOT-YET-STREAMED landblock near an edge → the cascade recurs (+ a Z free-fall). Evidence (launch5.log):

  • [snap] claim=0xC98C0028 pos=(113.666,190.259,22.010) branch=NO-LANDBLOCK (lbs=0) -> verbatim — arrival local Y=190.3, 1.7 m from the 192 edge, destination not streamed → cell marches 0xC98C → 0xC9FE (+2 lbY/tick), Z free-falls 22→19; outbound wire sends the marched cell C9FE0031 + compensating garbage localY=21684 → ACE MOVEMENT SPEED / failed transitionplayer stuck.
  • Same session, mid-block unstreamed arrival 0x977B000C (Y=73.8) → Z free-fall only, no march.
  • Hub 0x00070133 VALIDATED (cells resident) → fine.

Root (HYPOTHESIS — not apparatus-confirmed): outdoor teleport places immediately but the destination streams a few ticks later (streaming: dungeon EXIT-expand -> (201,140) logs just after placement). During the gap the resolve runs against an empty world; at an edge the player crosses into the unstreamed neighbour, where the carried anchor (which computes to (0,0,0) at the recentered center) gives no protection. Same root as the Z free-fall (#135/#138 placed-before-streamed gap).

⚠️ DO NOT guess-patch (original #145 burned 5 attempts). Apparatus first: add a diagnostic that logs the anchor value + the engagement-guard state in PhysicsEngine.ResolveWithTransition at the crossing tick (behind a probe flag), reproduce a 0xC98C-style arrival once, and confirm whether the anchor disengages (CellId/CellPosition landblock divergence) or the body is repositioned into a marched frame first. Then the fix is a known quantity.

Likely fix: a streaming-gap HOLD — freeze the per-tick resolve (no integration/gravity, no membership march) while the player's landblock is unloaded, until it streams in (a few ticks). The async-streaming equivalent of retail's synchronous load; would fix both the cascade and the Z plunge. Note: this may be subsumed by Work item B — the retail teleport flow holds until the world loads, so there's no unstreamed gap to begin with. Decide A-vs-B ordering at brainstorm.


Work item B — KEYSTONE FEATURE: the retail teleport flow

Symptoms (user-reported; all ONE root):

  1. Camera floats to the destination on teleport (disliked).
  2. Teleport takes too long / feels off vs retail.
  3. Player input is not locked during teleport.

Retail behaviour (user = oracle, treat as axiom): teleport plays an animation (portal effect), holds input + camera, waits for the world to load, then exits/places the player. Used for ALL teleportation: login, logout, death, portal.

Insight: acdream places immediately (raw placement → the floating camera + the unstreamed gap). A faithful retail teleport flow (anim + input/camera lock + hold-until-loaded + single exit point) fixes the float, the timing, the input-lock, and very likely Work item A. This is the keystone.

This is a FEATURE → resume superpowers:brainstorming (user is the retail oracle on look/feel). The 3-agent research (workflow wf_61501a7a-1d4) is DONE — findings below. Extend acdream's machinery, don't rebuild.

RETAIL flow (decomp-verified) — gmSmartBoxUI teleport animation state machine

TeleportAnimState enum (acclient.h:6871): the client plays a fade + portal-tunnel animation that COVERS the world-load wait — used by ALL of login / logout / death / portal:

TAS_OFF(0) → WORLD_FADE_OUT(1, ~1s) → TUNNEL_FADE_IN(2, ~1s) → TUNNEL(3 = HOLD for world load)
          → TUNNEL_CONTINUE(4, min ~2s) → TUNNEL_FADE_OUT(5, ~1s) → WORLD_FADE_IN(6, ~1s) → OFF
  • State machine: gmSmartBoxUI::UseTime (pc 219400); begin BeginTeleportAnimation (218888); end EndTeleportAnimation (218994).
  • The world-load HOLD is TAS_TUNNEL(3), gated on SmartBox::teleport_in_progress (pc 90815) = (player != 0) && (position_update_complete == 0); position_update_complete flips to 1 when the DDD interrogation / position update completes → animation advances. This is the "exit when the world has loaded" the user wants.
  • Input lock: PlayerModule::SetLockUI sets bit 0x1000000 in options2_ (pc 486713) for the whole animation; cleared at TAS_OFF.
  • Unification: death + logout share the flow via logOffRequested; login enters via RecvNotice_BeginEnterWorld; portal starts directly at TAS_TUNNEL (skips the fade-out). Sounds: Sound_UI_EnterPortal(0x6A) / ExitPortal(0x6B). Exit callback SendLoginCompleteNotification (pc 367516).
  • Missing from decomp: exact TELEPORT_ANIM_FADE_TIME / MIN_CONTINUE_TIME constants; the DDD code that sets position_update_complete.

acdream TODAY — what exists, what's missing

acdream ALREADY has the skeleton: PlayerState.PortalSpace input lock (PlayerMovementController.cs:840-854)

  • a hold-until-readiness controller (TeleportArrivalController.cs:56-105; GameWindow.OnTeleportStarted :5487, OnLivePositionUpdated/BeginArrival :5394, PlaceTeleportArrival :5443-5478, readiness :5419-5439).

The gaps (the actual work):

  1. NO animation / visual cover. During the hold the camera is frozen at the OLD spot, then PlaceTeleportArrival snaps both cameras (:5464-5469, no fade/lerp). That snap + the world streaming in is the "floating" the user dislikes. → Build a client teleport-anim state machine (TAS-shaped: fade-out → tunnel/hold → fade-in) that covers the wait.
  2. Readiness gate is too weak → this is the #145 residual. Outdoor readiness gates on SampleTerrainZ != null (:5436) — terrain heightmap can sample BEFORE the collision landblock cells load, so the player is placed onto a branch=NO-LANDBLOCK world → the unstreamed-arrival cascade + Z free-fall (Work item A). → Gate on the full landblock being loaded (teleport_in_progress-equivalent). Fixing this likely closes Work item A.
  3. yaw still updates during the hold (MouseLook not frozen, gotcha) — lock it too.
  4. Login / logout / death NOT unified. Login uses inline auto-entry guards (:1036-1065) that DUPLICATE teleport's readiness (:5419-5439) + recenter (:2874-2881 vs :5364-5370). Logout does not exist; death only feeds Chat.OnPlayerKilled (:2569), no game effect. → Route all four through one arrival/anim controller; de-dupe readiness + recenter.

Design direction (for the brainstorm)

Build a client-side teleport animation + arrival controller (port the TAS state machine shape) layered on the existing PortalSpace lock + TeleportArrivalController hold: (a) a fade/tunnel visual cover over the world-load wait (kills the float); (b) strengthen the readiness gate to the full landblock (kills the #145 residual + the Z free-fall); (c) unify login/logout/death/portal entry points + de-dupe the readiness/recenter logic; (d) tune timing so it doesn't feel longer than retail (the current 600-frame/10s timeout is the worst case).


Work item C — FPS leak after teleports (perf bug, SEPARATE)

Symptom: FPS drops low after a couple of teleports (cumulative). Hypothesis: a streaming/entity leak — landblocks or render/physics entities not freed on teleport, accumulating across round-trips (candidates: dungeon collapse/expand not cleaning up; RehydrateServerEntities / _lastSpawnByGuid accumulating; GpuWorldState entities not pruned). NEXT: capture FPS + landblock / entity counts across N teleports (ACDREAM_DUMP_LIVE_SPAWNS=1 + streaming logs + the DebugPanel counts), find what grows unbounded. Independent of A and B.


Pointers / apparatus

  • Logs (worktree root): launch5.log (#145 residual capture — the 0xC98C stuck-at-21684 run), launch4.log (Z free-fall + a 3-far-town round-trip), launch3/launch2 (clean streamed-arrival runs).
  • #145 record: docs/ISSUES.md #145 (reopened, with the trigger table) + the physics digest banner (claude-memory/project_physics_collision_digest.md).
  • Probes (no rebuild): ACDREAM_PROBE_CELL=1 ([cell-transit]), ACDREAM_PROBE_RESOLVE=1, ACDREAM_CAPTURE_RESOLVE=<path>, ACDREAM_DUMP_LIVE_SPAWNS=1.
  • Decomp oracle: docs/research/named-retail/ (grep class::method); for the teleport flow, look at the portal/teleport + DDD-loading + animation paths.
  • Wire cross-check: references/holtburger (client teleport flow) + references/ACE (server teleport / MOVEMENT SPEED validation — the failed transition rejects).

Suggested order (brainstorm to confirm)

  1. B first (retail teleport flow) — it's the keystone and likely subsumes A. Brainstorm → spec → build.
  2. Re-test A: if the hold-until-loaded flow eliminates the unstreamed gap, the #145 residual is gone; if a narrow edge case survives, apply the diagnostic + targeted hold.
  3. C (FPS leak) — independent; can run in parallel as its own investigation.
  4. (Background) Slices 46 — the invisible Option-B completeness, unrelated to this cluster.