docs(teleport-flow): design — unified retail TeleportAnimState flow (Work item B keystone)
Brainstormed + decomp-researched the retail teleport flow, the keystone of the #138/#145 teleport cluster. Five oracle calls locked: full retail TAS; hold-until-landblock-loaded readiness (folds in the #145 residual / Work item A); unify all four entry points (login/logout/death/portal); build the literal portal swirl this pass; logout = animation + 0xF653 + disconnect (char-select UI deferred). Grounded in a verified 5-agent decomp pass (workflow wf_f0c07c93-7aa): the 7-state TeleportAnimState machine + golden constants (FADE=1s, MIN/MAX CONTINUE=2/5s, FPS=40), the teleport_in_progress hold gate, per-entry start states, sounds, input lock, exit (LoginComplete 0xA1). Load-bearing acdream facts re-verified personally: AddLandblock atomicity, streaming-progresses- during-hold, outdoor place-immediately. Spec: docs/superpowers/specs/2026-06-21-retail-teleport-flow-design.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
adaec1845f
commit
aab9ddaf6b
1 changed files with 326 additions and 0 deletions
326
docs/superpowers/specs/2026-06-21-retail-teleport-flow-design.md
Normal file
326
docs/superpowers/specs/2026-06-21-retail-teleport-flow-design.md
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
# Design — Retail teleport flow (the unified `TeleportAnimState` controller)
|
||||||
|
|
||||||
|
**Date:** 2026-06-21
|
||||||
|
**Feature:** Work item B from `docs/research/2026-06-21-teleport-issues-handoff.md` — the keystone of the
|
||||||
|
teleport-issues cluster (#138, #145 residual, the floating-camera / input-not-locked / "takes too long"
|
||||||
|
symptoms).
|
||||||
|
**Milestone:** M1.5 "Indoor world feels right" (far-town + dungeon round-trips).
|
||||||
|
**Status:** APPROVED design — ready for implementation plan.
|
||||||
|
**Decision authority:** user (retail oracle), 2026-06-21. Five binding calls:
|
||||||
|
1. Visual cover = **full retail TAS** (fade → portal tunnel → fade), not a simplified fade.
|
||||||
|
2. Readiness gate = **hold outdoor until the physics landblock is loaded** (folds Work item A / #145 residual in).
|
||||||
|
3. Unification = **all four** entry points (login / logout / death / portal) this pass.
|
||||||
|
4. Tunnel swirl = **resolve the dat asset + build the literal 3D swirl this pass** (net-new sub-viewport infra).
|
||||||
|
5. Logout = **animation + `0xF653` + clean disconnect now**; the char-select-return UI is deferred (§9).
|
||||||
|
|
||||||
|
**Relates to:** #145 (the readiness-gate fix subsumes the reopened residual / Work item A), #138 (teleport-OUT
|
||||||
|
of a dungeon rides the same hold-until-loaded path), Work item C (FPS leak — separate, not addressed here).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
A teleport in acdream **places the player immediately** on the server-authoritative position, then lets the
|
||||||
|
world stream in around them. Three user-reported symptoms all trace to that one design choice:
|
||||||
|
|
||||||
|
1. **Camera floats** to the destination — the old camera is snapped to a placed-but-unstreamed spot and the
|
||||||
|
world materializes around it.
|
||||||
|
2. **Feels too long / wrong vs retail** — there is no animation pacing the wait; it is a raw snap plus a
|
||||||
|
visible streaming-in.
|
||||||
|
3. **Input is not locked** during the transition (yaw still moves — §3.6).
|
||||||
|
|
||||||
|
A fourth, deeper consequence is the **#145 residual / Work item A**: an immediate placement onto a
|
||||||
|
**not-yet-streamed** landblock near an edge lands the player on a `branch=NO-LANDBLOCK` empty world, where the
|
||||||
|
per-tick resolve marches the cell label and free-falls Z (the `0xC98C…` stuck-at-`−21684` capture in
|
||||||
|
`launch5.log`).
|
||||||
|
|
||||||
|
Retail does none of this. It plays a **fade + portal-tunnel animation that COVERS the world-load wait**, holds
|
||||||
|
input and the camera, **waits until the world has actually loaded**, then places the player at a single exit
|
||||||
|
point. The same flow drives **login, logout, death, and portal** — it is the client's one "go somewhere"
|
||||||
|
primitive. Porting it faithfully fixes the float, the timing, the input-lock, **and** the #145 residual (the
|
||||||
|
hold removes the unstreamed-arrival gap entirely).
|
||||||
|
|
||||||
|
acdream already has the *skeleton*: `PlayerState.PortalSpace` (input freeze) + `TeleportArrivalController`
|
||||||
|
(hold-until-ready) + `TeleportArrivalRules` (the readiness verdict). What is missing is the **animation /
|
||||||
|
visual cover**, a **strong enough readiness gate**, and **unification** of the four entry points (which today
|
||||||
|
duplicate the readiness + recenter logic and, for logout/death, barely exist).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Retail reference (decomp-verified)
|
||||||
|
|
||||||
|
All citations are to `docs/research/named-retail/acclient_2013_pseudo_c.txt` (line ≈ pseudo-C line) and
|
||||||
|
`acclient.h`, captured by the 5-agent research workflow `wf_f0c07c93-7aa` and spot-verified.
|
||||||
|
|
||||||
|
### 2.1 The `TeleportAnimState` machine
|
||||||
|
|
||||||
|
`enum TeleportAnimState` (`acclient.h:6871`) — 7 states. The machine lives in `gmSmartBoxUI::UseTime`
|
||||||
|
(pc 0x004d6e30, ≈219400); `BeginTeleportAnimation` (218888) enters it; `EndTeleportAnimation` (218994)
|
||||||
|
advances a running tunnel to `TUNNEL_CONTINUE`.
|
||||||
|
|
||||||
|
```
|
||||||
|
TAS_OFF(0)
|
||||||
|
→ TAS_WORLD_FADE_OUT(1) ~1s fade the world to black
|
||||||
|
→ TAS_TUNNEL_FADE_IN(2) ~1s fade the portal tunnel in
|
||||||
|
→ TAS_TUNNEL(3) HOLD wait for the world to load (the gate)
|
||||||
|
→ TAS_TUNNEL_CONTINUE(4) 2–5s min/max spin-hold after load
|
||||||
|
→ TAS_TUNNEL_FADE_OUT(5) ~1s fade the tunnel out
|
||||||
|
→ TAS_WORLD_FADE_IN(6) ~1s fade the (now-loaded) world in
|
||||||
|
→ TAS_OFF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Golden constants** (read directly from the binary at the cited VAs):
|
||||||
|
|
||||||
|
| Constant | Value | VA | Role |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `TELEPORT_ANIM_FADE_TIME` | **1.0 s** | 0x007BD278 | duration of each of the 4 fade segments |
|
||||||
|
| `TELEPORT_ANIM_MIN_CONTINUE_TIME` | **2.0 s** | 0x007BD268 | `TUNNEL_CONTINUE` floor before `FADE_OUT` |
|
||||||
|
| `TELEPORT_ANIM_MAX_CONTINUE_TIME` | **5.0 s** | 0x007BD270 | forces `FADE_OUT` even if not loaded |
|
||||||
|
| `TELEPORT_ANIM_FPS` | **40.0** | 0x007BD280 | swirl `CPhysicsObj` animation rate |
|
||||||
|
| `TRANSITION_VIEW_PLANE_DISTANCE` | **0.001** | 0x007BD260 | the near-plane value that reads as "black" |
|
||||||
|
|
||||||
|
**Timing budget that results (this is "faithful", a floor we cannot shave):**
|
||||||
|
- Portal / login / **death** (enter at `TUNNEL`): `TUNNEL`(load) + `CONTINUE`(≥2s) + `FADE_OUT`(1s) +
|
||||||
|
`WORLD_FADE_IN`(1s) ≈ **4 s + load**.
|
||||||
|
- **Logout** (enters at `WORLD_FADE_OUT`): + `WORLD_FADE_OUT`(1s) + `TUNNEL_FADE_IN`(1s) on the front ≈ **6 s + load**.
|
||||||
|
|
||||||
|
### 2.2 The fade mechanism
|
||||||
|
|
||||||
|
The fade is **not** an alpha quad in retail — it collapses the camera near-plane via
|
||||||
|
`Render::set_vdst` (≈342121) to `TRANSITION_VIEW_PLANE_DISTANCE` (0.001), which renders the scene black.
|
||||||
|
Interp: `vdst = lerp(gameVDist, 0.001, GetAnimLevel(t)/1024)` on fade-out and the reverse on fade-in, with
|
||||||
|
`t = clamp(elapsed / FADE_TIME, 0, 1)`. `UIGlobals::GetAnimLevel` is a table-driven ease curve mapping
|
||||||
|
`t∈[0,1]` → `int∈[0,1024]` (exact curve unread — see §8). The **visible result is identical to a fade to
|
||||||
|
black**, which is what acdream will reproduce (§3.3).
|
||||||
|
|
||||||
|
### 2.3 The tunnel swirl
|
||||||
|
|
||||||
|
During the four `TUNNEL*` states the world is hidden (`SmartBox::Hide`) and a **portal viewport is shown**:
|
||||||
|
a `UIElement_Viewport` (Type 0xD, child id `0x10000436`) hosting a `CPhysicsObj` made from
|
||||||
|
`GetDIDByEnum(0x10000001, 7)` (the portal setup object), with a distant light, camera at `(0.24, −2.7, 0.88)`,
|
||||||
|
playing animation `GetDIDByEnum(0x10000002, 7)` at **40 fps**, plus a **randomized camera-direction rotation**
|
||||||
|
each segment (`CreatureMode::SetCameraDirection_Degrees`, new random end-angle + duration via `RandDouble`).
|
||||||
|
The "In Portal Space - Please Wait..." string is (re)displayed each time the rotation segment restarts.
|
||||||
|
The `GetDIDByEnum` ids are runtime EnumIDMap lookups — the concrete dat ids must be resolved by a runtime
|
||||||
|
trace (§8, the swirl slice's prerequisite).
|
||||||
|
|
||||||
|
### 2.4 The "world has loaded" signal (the hold gate)
|
||||||
|
|
||||||
|
`SmartBox::teleport_in_progress` (pc 90815) `= (player != 0) && (position_update_complete == 0)`.
|
||||||
|
`TAS_TUNNEL` holds while this is true. `position_update_complete` is **cleared** on receipt of a server
|
||||||
|
teleport (`SmartBox::HandlePlayerTeleport`, 91200, sets `waiting_for_teleport = 1`); it is **set to 1** in
|
||||||
|
`SmartBox::UseTime` (94184) once `waiting_for_teleport == 0` — which clears when the player's physics state
|
||||||
|
settles (`DoSetState`, state bit `0x40` cleared). i.e. retail advances on **state-settled**, not a timer. The
|
||||||
|
acdream analogue is "the destination collision landblock is resident" (§3.4) — the async-streaming equivalent
|
||||||
|
of retail's synchronous load.
|
||||||
|
|
||||||
|
### 2.5 Per-entry start state, sounds, lock, exit
|
||||||
|
|
||||||
|
| Entry | Start state | Trigger (decomp) |
|
||||||
|
|---|---|---|
|
||||||
|
| **Portal** | `TAS_TUNNEL` (skips world-fade-out) | `teleport_in_progress` 0→1 from the server teleport message (219440) |
|
||||||
|
| **Login** | `TAS_TUNNEL` | same predicate at first tick; exits fast (`waiting_for_teleport==0`) (85, 219440) |
|
||||||
|
| **Logout** | `TAS_WORLD_FADE_OUT` (full fade-out first) | `logOffRequested && cur_time ≥ logOffRequestTime` (219415) |
|
||||||
|
|
||||||
|
- **Death is a server teleport**, not a client flag: ACE teleports the corpse-owner to the sanctuary after the
|
||||||
|
death anim (`Player_Death.cs:247`, `Player_Location.cs:686`) — the client sees an ordinary teleport. So death
|
||||||
|
rides the **portal** path, entering at `TAS_TUNNEL` (NOT the `logOffRequested` `WORLD_FADE_OUT` path, which is
|
||||||
|
logout-only — retail shares the `logOffRequested` *code* between logout and self-destruct, but client-observed
|
||||||
|
death arrives as a teleport).
|
||||||
|
- **Sounds:** `Sound_UI_EnterPortal` in `BeginTeleportAnimation` (218903); `Sound_UI_ExitPortal` at
|
||||||
|
`TAS_TUNNEL_FADE_OUT → WORLD_FADE_IN` (219745). (The `0x6A/0x6B` ids in the handoff are **wrong** for our
|
||||||
|
enum — they map to creature sounds; the real Wave ids come from the dat SoundTable — §8.)
|
||||||
|
- **Input lock:** `PlayerModule::SetLockUI` sets `options2_` bit `0x1000000` for the whole animation;
|
||||||
|
`SetTeleportInProgress(1)` also bumps a UI busy-count (`IncrementBusyCount`, 361733).
|
||||||
|
- **Exit:** at `WORLD_FADE_IN` completion → `SendLoginCompleteNotification` (367516) (sends GameAction `0xA1`
|
||||||
|
to the server) — fired on **both** initial login and after **every** portal (holtburger confirms:
|
||||||
|
`messages.rs:464,480`). Gated on the player weenie + `AllContainedObjectsExist`.
|
||||||
|
- **Camera is NOT frozen by the TAS machine** — the regular `UpdateCamera` keeps running; the "freeze" is just
|
||||||
|
the world not being drawn during the tunnel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. acdream end-state design
|
||||||
|
|
||||||
|
### 3.1 Components (boundaries)
|
||||||
|
|
||||||
|
Two pure pieces + thin App wiring, mirroring the existing `TeleportArrivalController` (pure hold) +
|
||||||
|
`TeleportArrivalRules` (pure verdict) split.
|
||||||
|
|
||||||
|
- **`TeleportAnimSequencer`** (`AcDream.Core`, pure, no GL/dat/net — **unit-tested against §2.1 golden
|
||||||
|
timings**). The 7-state machine. Per `Tick(dt, worldReady, …)` it returns an immutable snapshot:
|
||||||
|
```csharp
|
||||||
|
public readonly record struct TeleportAnimSnapshot(
|
||||||
|
TeleportAnimState State,
|
||||||
|
float FadeAlpha, // 0 = clear world, 1 = full black
|
||||||
|
bool ShowTunnel, // true during TUNNEL_FADE_IN..TUNNEL_FADE_OUT
|
||||||
|
bool ShowPleaseWait); // true during TUNNEL_CONTINUE
|
||||||
|
public enum TeleportAnimEvent { None, PlayEnterSound, PlayExitSound, Place, FireLoginComplete }
|
||||||
|
```
|
||||||
|
`Begin(TeleportEntryKind kind)` sets the start state per §2.5. The sequencer owns the durations/transitions;
|
||||||
|
it knows nothing about *how* the fade or tunnel are drawn. `worldReady` is supplied by the controller (the
|
||||||
|
arrival hold). It emits `Place` when leaving `TUNNEL` (world loaded) and `FireLoginComplete` at `OFF`.
|
||||||
|
|
||||||
|
- **`TeleportFlowController`** (`AcDream.App.World`). Owns one `TeleportAnimSequencer` + the existing
|
||||||
|
`TeleportArrivalController`. Each frame: ask the arrival controller whether the destination is ready, feed
|
||||||
|
that as `worldReady`, tick the sequencer, then apply the snapshot/events: drive the fade overlay alpha,
|
||||||
|
show/hide the tunnel viewport, play sounds, hold/clear the input lock, and on `Place` invoke the existing
|
||||||
|
placement (`PlaceTeleportArrival`), on `FireLoginComplete` send GameAction `0xA1`. It is the **single owner**
|
||||||
|
of "a teleport is happening"; the four entry points all call into it.
|
||||||
|
|
||||||
|
- **`TeleportFadeOverlay`** (`AcDream.App.UI`) — a `UiRoot` child (last, top-most among retail-UI) that fills
|
||||||
|
the screen with `(0,0,0,FadeAlpha)` via `TextRenderer.DrawRect` inside the existing `UiHost.Draw` pass
|
||||||
|
(`GameWindow.cs:8885`). No new GL infra.
|
||||||
|
|
||||||
|
- **`PortalTunnelView`** (`AcDream.App.Rendering`) — **net-new**: an offscreen 3D sub-scene rendering the
|
||||||
|
resolved swirl `CPhysicsObj` at 40 fps with the randomized camera spin, blitted to the screen during
|
||||||
|
`ShowTunnel`. This is the heaviest slice and is gated on the asset trace (§7, §8).
|
||||||
|
|
||||||
|
Relationship to the existing controller: `TeleportArrivalController` stays the **pure hold** ("is the world
|
||||||
|
ready, and place when so"); `TeleportFlowController` is the **visual/timing/lock state machine** wrapped
|
||||||
|
around it. The arrival controller's "ready" verdict is the sequencer's `worldReady` gate (the
|
||||||
|
`teleport_in_progress` analogue).
|
||||||
|
|
||||||
|
### 3.2 State machine
|
||||||
|
|
||||||
|
Port §2.1 verbatim: states, the 1.0/2.0/5.0 s constants, the per-entry start states (§2.5). `FadeAlpha` is
|
||||||
|
`lerp` per §2.2 with `t = clamp(elapsed/FADE_TIME, 0, 1)` and a **smoothstep** ease in place of retail's
|
||||||
|
`GetAnimLevel` table (§8 — upgrade to the real table if a cdb read resolves it; *register row*). The
|
||||||
|
`TUNNEL → TUNNEL_CONTINUE` edge fires when `worldReady` first becomes true (retail's `EndTeleportAnimation`);
|
||||||
|
`MAX_CONTINUE_TIME` forces progress if it never does (the safety net, replacing the current 600-frame timeout
|
||||||
|
semantics).
|
||||||
|
|
||||||
|
### 3.3 Visual cover
|
||||||
|
|
||||||
|
- **Fade:** the `TeleportFadeOverlay` alpha-black quad (§3.1). *Register row: black-fade via alpha overlay,
|
||||||
|
not retail's near-plane collapse — visually identical, no camera-projection surgery.*
|
||||||
|
- **Tunnel swirl:** `PortalTunnelView` (§3.1) rendering the resolved swirl object. Prerequisite: a cdb trace
|
||||||
|
on a live retail teleport (bp `CPhysicsObj::makeObject`) to capture the concrete `GetDIDByEnum(0x10000001,7)`
|
||||||
|
/ `(0x10000002,7)` dat ids (§8). During `ShowTunnel` the world is not drawn (sequencer drives this), matching
|
||||||
|
retail `SmartBox::Hide`.
|
||||||
|
- **"In Portal Space - Please Wait…"** text during `TUNNEL_CONTINUE` (`ShowPleaseWait`), drawn by the
|
||||||
|
retail-UI text layer.
|
||||||
|
|
||||||
|
### 3.4 Readiness gate — Call-2 (folds in Work item A / #145 residual)
|
||||||
|
|
||||||
|
Today `TeleportArrivalRules.Decide` returns `Ready` for **any outdoor** destination → immediate placement →
|
||||||
|
the NO-LANDBLOCK gap. The fix:
|
||||||
|
|
||||||
|
- Add `PhysicsEngine.IsLandblockLoaded(uint landblockId) => _landblocks.ContainsKey(landblockId)`. Because
|
||||||
|
`AddLandblock` registers terrain + collision cells + portals **atomically** (`PhysicsEngine.cs:70`),
|
||||||
|
`ContainsKey` ⟹ the collision mesh is present and resolvable.
|
||||||
|
- `TeleportArrivalRules.Decide` gains an `outdoorLandblockReady` input; the outdoor branch returns `Ready`
|
||||||
|
**iff** the destination landblock is loaded, else `NotReady` (hold). Indoor still gates on
|
||||||
|
`IsSpawnCellReady` (unchanged). `Impossible` (un-hydratable claim) and the timeout safety-net are unchanged.
|
||||||
|
- **Why this is safe and self-resolving:** streaming **progresses during the hold** — `_streamingController.Tick`
|
||||||
|
runs unconditionally with the observer pinned to the destination (`GameWindow.cs:7420-7531`), and the #145
|
||||||
|
`RemoveLandblock` of the stale source clears `CurrCell` so the dungeon-streaming gate ExitDungeonExpands to
|
||||||
|
the destination (`PhysicsEngine.cs:84-95`). The gate flips `Ready` the same frame `AddLandblock` fires
|
||||||
|
(`_teleportArrival.Tick` runs after the streaming drain — `GameWindow.cs:7551`). The player is therefore
|
||||||
|
**never** placed onto an empty world → no cell-march, no Z free-fall. The 10 s timeout remains a loud net.
|
||||||
|
- **Verified-by-apparatus first** (task #4): before relying on this, add a probe that logs the readiness verdict
|
||||||
|
transition + `_landblocks.Count` and confirms on a `0xC98C…`-style edge arrival that the gate flips `Ready`
|
||||||
|
*before* placement (pair with `ACDREAM_PROBE_RESOLVE` / `ACDREAM_CAPTURE_RESOLVE`). No guess-patch (the
|
||||||
|
original #145 burned 5 attempts).
|
||||||
|
|
||||||
|
This is the single change that subsumes Work item A. After it lands, re-test the `launch5.log` scenario.
|
||||||
|
|
||||||
|
### 3.5 Unification + de-dup
|
||||||
|
|
||||||
|
The login auto-entry readiness (`GameWindow.cs:1036-1064`) and the teleport readiness
|
||||||
|
(`TeleportArrivalReadiness`, `:5419-5438`) are **verbatim duplicates** (the code comment says so). Extract a
|
||||||
|
single `ComputeArrivalReadiness(destPos, destCell)` used by both sites. The two `_liveCenter` recenters
|
||||||
|
(login `:2874-2882`, teleport `:5364-5371`) stay in their respective handlers but share one helper. This kills
|
||||||
|
the drift hazard where #135/#145-class bugs lived.
|
||||||
|
|
||||||
|
### 3.6 Input / camera lock
|
||||||
|
|
||||||
|
PortalSpace already freezes keyboard turns (the `Update` early-return precedes the yaw block). Two yaw leaks
|
||||||
|
remain (the handoff gotcha), both in the Silk mouse-move handler, bypassing the `Update` guard:
|
||||||
|
- **MMB mouse-look** (`GameWindow.cs:1098`) → `_mouseLook.ApplyDelta` writes `Yaw`.
|
||||||
|
- **RMB orbit** (`GameWindow.cs:1120`) → writes `_retailChaseCamera.YawOffset` / `_chaseCamera.YawOffset`.
|
||||||
|
|
||||||
|
Fix: a `State == PortalSpace` early-return at the top of each branch. (No change to PortalSpace logic itself.)
|
||||||
|
|
||||||
|
### 3.7 Sounds
|
||||||
|
|
||||||
|
Resolve the EnterPortal / ExitPortal Wave ids from the dat SoundTable (§8) and play via
|
||||||
|
`OpenAlAudioEngine.PlayUiWave` (the UI-pool path; the `IAudioEngine.PlayUi(SoundId)` stub is a no-op — do not
|
||||||
|
use it). Enter on `PlayEnterSound`, exit on `PlayExitSound`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The four entry points
|
||||||
|
|
||||||
|
| Entry | Start | Trigger in acdream | Scope this pass |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Portal** | `TUNNEL` | `OnTeleportStarted` (0xF751) **or** the large-jump branch of `OnLivePositionUpdated` | Full TAS — the keystone |
|
||||||
|
| **Death** | `TUNNEL` | server teleport-to-lifestone (same path as portal) | Free — verify it routes through the controller (§8 confirms the trigger) |
|
||||||
|
| **Login** | `TUNNEL` | the #107 auto-entry place (`EnterPlayerModeNow`) | Unify: route through the controller (tunnel→fade-in on spawn) + the §3.5 de-dup. **Regression-gated: login must stay FPS-steady.** |
|
||||||
|
| **Logout** | `WORLD_FADE_OUT` | a new "log off" command | Animation + `0xF653` + clean disconnect. **Char-select-return UI deferred (§9).** |
|
||||||
|
|
||||||
|
The controller keys off both the `0xF751` start **and** the existing large-position-jump detection, so a
|
||||||
|
teleport that arrives only as an `UpdatePosition` (no `0xF751`) still triggers the flow — more robust than
|
||||||
|
relying on the opcode alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Divergence-register rows (added in the implementing commits)
|
||||||
|
|
||||||
|
1. **Fade via alpha overlay, not near-plane collapse** — adaptation; visually identical (§3.3).
|
||||||
|
2. **`GetAnimLevel` ease approximated as smoothstep** — approximation; retire if the 1024-entry table is
|
||||||
|
resolved by a cdb read (§8).
|
||||||
|
3. **Tunnel-swirl camera/rotation parameters approximated** — approximation; the exact `RandDouble` angular
|
||||||
|
range + the frame-120 exit detail were medium-confidence in the decomp (§8); tune to match a live trace.
|
||||||
|
4. **Logout returns to disconnect/window-close, not a char-select screen** — stopgap; retired when the
|
||||||
|
char-select / re-login UI is built (§9).
|
||||||
|
|
||||||
|
(Any row that a later port retires is deleted in the same commit, per the register rules.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing & verification
|
||||||
|
|
||||||
|
- **Unit:** `TeleportAnimSequencer` against §2.1 golden timings — each transition fires at the right elapsed
|
||||||
|
time; per-entry start states; `worldReady` gating of `TUNNEL`; `MAX_CONTINUE` force; the event sequence
|
||||||
|
(Enter sound → Place → Exit sound → LoginComplete).
|
||||||
|
- **Readiness conformance:** `TeleportArrivalRules.Decide` truth table incl. the new `outdoorLandblockReady`
|
||||||
|
axis (outdoor-not-loaded → `NotReady`; outdoor-loaded → `Ready`; indoor unchanged; impossible → `Impossible`).
|
||||||
|
- **Apparatus (task #4):** confirm the gate flips `Ready` before placement on an edge arrival (§3.4); re-run
|
||||||
|
the `launch5.log` `0xC98C` scenario — expect no march, no Z free-fall.
|
||||||
|
- **Build/test green**, then **visual verification by the user** (the one mandatory stop): portal feel, the
|
||||||
|
swirl, login spawn, death-to-lifestone, logout fade — does it match retail?
|
||||||
|
|
||||||
|
## 7. Implementation slicing (ordered; writing-plans details these)
|
||||||
|
|
||||||
|
1. **`TeleportAnimSequencer`** (pure Core) + unit tests — no wiring yet.
|
||||||
|
2. **Readiness gate** (`IsLandblockLoaded` + `Decide` change) + apparatus probe + the `launch5.log` re-test.
|
||||||
|
*(Lands the #145 residual / Work item A fix independently of the visuals.)*
|
||||||
|
3. **`TeleportFlowController` + `TeleportFadeOverlay`** — wire portal through the sequencer with the fade
|
||||||
|
(tunnel states show black until slice 6). Kills the float + input float for portals.
|
||||||
|
4. **Input/camera yaw freeze** (§3.6) + sounds (§3.7) — resolve Wave ids first.
|
||||||
|
5. **Unify login + death + logout** (§3.5, §4) — de-dup readiness/recenter; logout command + `0xF653`.
|
||||||
|
6. **`PortalTunnelView`** (the swirl sub-viewport) — **prerequisite: the asset-id cdb trace (§8).** Slot it
|
||||||
|
into the tunnel states. Heaviest, asset-dependent; last so everything else ships without it.
|
||||||
|
|
||||||
|
## 8. Open prerequisites / questions (resolve during build, not blocking the spec)
|
||||||
|
|
||||||
|
- **Swirl dat asset ids** — cdb-trace `CPhysicsObj::makeObject` on a live retail teleport to capture the
|
||||||
|
resolved `GetDIDByEnum(0x10000001,7)` / `(0x10000002,7)` ids. Gates slice 6.
|
||||||
|
- **EnterPortal / ExitPortal Wave ids** — from the dat SoundTable (the handoff's `0x6A/0x6B` are creature
|
||||||
|
sounds in our enum). Gates slice 4's audio.
|
||||||
|
- **`GetAnimLevel` curve** — cdb read of the 1024-entry table, else smoothstep (register row 2).
|
||||||
|
- **Death trigger confirmation** — confirm ACE sends a teleport (`0xF751`/position-jump) the controller already
|
||||||
|
catches on death; the large-jump fallback covers it regardless.
|
||||||
|
- **Tunnel exit detail** — the frame-120 + min/max-continue compound exit was medium-confidence; tune to a live
|
||||||
|
trace (register row 3).
|
||||||
|
|
||||||
|
## 9. Out of scope (deferred)
|
||||||
|
|
||||||
|
- **Char-select / re-login UI.** acdream has no login or character-select screen — login is a synchronous
|
||||||
|
auto-select at startup; the only exit today is window-close. Faithful logout-to-char-select requires building
|
||||||
|
that screen + session-teardown-and-restart + re-entry. This pass builds the logout **animation + `0xF653` +
|
||||||
|
clean disconnect** only; the return-to-char-select UI is its own future feature (register row 4 tracks the
|
||||||
|
stopgap).
|
||||||
|
- **Work item C** (FPS leak after teleports) — separate perf investigation, unaffected by this design.
|
||||||
|
- **#145 Slices 4–6** (invisible Option-B completeness) — unrelated background work.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue