fix(world): match retail portal passage and exit warp
This commit is contained in:
parent
eab23cbdd1
commit
842bd89c16
11 changed files with 355 additions and 28 deletions
|
|
@ -609,10 +609,21 @@ and no world point lights for this pass. Chat, gameplay windows, toolbar,
|
|||
cursor, and input continue normally above it. The synthetic object never
|
||||
enters `LiveEntityRuntime`, collision, picking, radar, or server GUID state.
|
||||
|
||||
`TeleportViewPlaneController` is the App-layer projection adapter for retail
|
||||
`SmartBox::SetOverrideFovDistance` / `Render::set_vdst`. It captures the active
|
||||
game projection at teleport begin, then applies the same table-eased
|
||||
view-plane distance and near plane to the portal and world viewports through
|
||||
the four fade states. Portal camera direction is a roll around its local AC
|
||||
`+Y` forward axis, so the animated scene remains a passage rather than yawing
|
||||
away from it. Destination placement also calls the retail chase camera's
|
||||
`set_viewer(player, reset_sought=1)` equivalent; the normal damped/swept camera
|
||||
path then re-extends from the arriving player.
|
||||
|
||||
The destination residency gate supplies retail's `EndTeleportAnimation` edge
|
||||
once asynchronous streaming is ready. Player placement remains authoritative
|
||||
and separate from presentation; arrival does not reset the character animation
|
||||
sequence. See `docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||||
and separate from presentation; arrival resets camera viewer state but does
|
||||
not reset the character animation sequence. See
|
||||
`docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||||
|
||||
## Roadmap Model
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-35 | `MotionTableManager.PerformMovement`'s unhandled-type default case returns the named sentinel `0xFFFFFFFF` (`MotionTableManagerError.NotHandled`); retail's compiled code leaks the `CSequence*` pointer reinterpreted as the return code (BN-confirmed artifact, dead/unreachable — callers gate on type first) (R2-Q3, 2026-07-02) | `src/AcDream.Core/Physics/Motion/MotionTableManager.cs` (`PerformMovement` default case) | No retail caller consults the return value for unhandled types (RawCommand/StopRawCommand/MoveTo\*/TurnTo\* route elsewhere); returning a stable non-zero sentinel preserves the only observable contract (non-zero = not success) without fabricating a pointer-shaped number | If a future port wires a caller that passes unhandled types AND branches on the exact return value, it would see `0xFFFFFFFF` where retail saw an arbitrary pointer — flag at that port | `PerformMovement` 0x0051c0b0 (`r2-motiontable-decomp.md` §11 default-case note) |
|
||||
| AD-36 | `IMotionDoneSink.MotionDone` consumed for CREATURE-class entities only: R3-W2 binds the seam to the entity's `MotionInterpreter.MotionDone` (player via `PlayerMovementController.Motion`, remotes via `RemoteMotion.Motion`, resolved at fire time); interp-less entities (statics that never receive a UM/UP and so never get a `RemoteMotion`) keep a diagnostic-recorder-only target — retail gives every CPhysicsObj a MovementManager/CMotionInterp (R2-Q4 seam, narrowed R3-W2, 2026-07-02; R5-V5 gave every `RemoteMotion`/player ONE literal `MovementManager` facade, so the residue is only the no-RemoteMotion class) | `src/AcDream.App/Rendering/GameWindow.cs` (TickAnimations MotionDoneTarget bind) | Motion for entities without a `RemoteMotion` (never UM/UP-touched) completes via the manager queue alone; nothing consumes their MotionDone until every sequencer-owning entity gets a host/`RemoteMotion` (doors DO have one since the R4-V5 door fix — first UM creates it) | An entity behavior depending on pending_motions bookkeeping in that no-RemoteMotion class (none known) would silently no-op | `CPhysicsObj::MotionDone` 0x0050fdb0; retire when every sequencer-owning entity constructs a `RemoteMotion`/host (post-M1.5 entity-class unification; R5-V5 closed the facade half) |
|
||||
| AD-37 | Camera rotation state is a forward VECTOR (nlerp + normalize; roll always 0, up = world Z); retail's sought carries a full Frame and slerps quaternions (`Frame::interpolate_rotation` shortest-path slerp with 2e-4 nlerp fallback). The dead-band compares forward-vector distance against the same 2e-4 epsilon retail applies per quaternion component | `src/AcDream.App/Rendering/RetailChaseCamera.cs` (`_dampedForward`, `ApplyConvergenceSnap`) | The chase camera never rolls (heading frames are Z-up by construction), so a forward vector spans the reachable rotation space; identified (not introduced) during the #180 UpdateCamera tail reading | If a future camera mode needs roll (death cam, cutscene) the vector state can't represent it; large-angle per-frame turns nlerp (chord) vs slerp (arc) — imperceptible at 0.45-stiffness step sizes | `Frame::interpolate_rotation` 0x00535390, `Frame::close_rotation` 0x00455d70; pseudocode doc 2026-07-06-camera-sought-position |
|
||||
| AD-38 | First camera frame seats sought = viewer = the full-length target eye (starts converged); retail hard resets both to the PLAYER's position (`set_viewer(pos, reset_sought=1)`) at login/teleport/cell-loss, so the retail camera visibly re-extends outward from the head over ~1 s | `src/AcDream.App/Rendering/RetailChaseCamera.cs` (`_initialised` branch) | acdream teleports don't reset the camera object today; the pivot-anchored sweep self-heals in one convergence, and the login zoom-out is cosmetic. Identified (not introduced) during the #180 reading | Login/teleport first frames show a fully-extended boom where retail shows a zoom-out; if a teleport lands the stale sought behind distant geometry the first sweeps clip until convergence (sub-second) | `SmartBox::set_viewer` 0x00452c40 (reset_sought=1 sites pc:92775, pc:92886); retire by wiring teleport → sought reset |
|
||||
| AD-39 | The `frames_stationary_fall` ladder + fsf≥3 UP-contact-plane manufacture runs AFTER acdream's fused LKCP-restore/contact-marking block, deriving retail's `_redo` as `cleanAdvance \|\| OnWalkable`; retail (ACE Transition.cs:1029-1061) interleaves the fsf block BETWEEN the LKCP-restore (sets `_redo`) and the contact-marking (reads the manufactured plane) (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ValidateTransition` fsf tail) | acdream deliberately fused ACE's separate LKCP-restore + contact-mark blocks (the L.2.3c/L.2.4/A6.P3 contact-retention divergences); running the ladder after them and re-marking grounding inside the manufacture branch is semantically equal (a grounded wall-slide is not a stuck-fall in either arrangement) without disturbing those hard-won fixes | If a future contact-retention change alters when OnWalkable is set relative to the ladder, `_redo` could misclassify a frame (grounded-jam mistaken for stuck-fall → spurious velocity zero, or vice-versa) — the fsf conformance tests pin the current arrangement | `CTransition::validate_transition` 0x0050aa70 pc:272625-656; ACE Transition.cs:1029-1061 |
|
||||
| AD-40 | The fsf `Stationary*` transient-bit encode (fsf→0x10/0x20/0x40) lives in the Core resolve writeback (`PhysicsEngine.ResolveWithTransition`), co-located with the fsf computation; retail encodes it in `handle_all_collisions` (pc:282737-758). Also: `PhysicsBody.CachedVelocity` is computed at the player chokepoint but not yet consumed — outbound wire velocity still uses the existing `get_state_velocity` path, not retail's cached_velocity source (#182 rebuild, 2026-07-07) | `src/AcDream.Core/Physics/PhysicsEngine.cs` (writeback); `src/AcDream.App/Input/PlayerMovementController.cs` (`CachedVelocity`) | Encoding in the writeback keeps the seed→ladder→writeback→seed round-trip self-contained in Core (testable without the App loop); the bit values + timing are identical to retail's (set after fsf is final, before the next resolve). CachedVelocity is faithful to carry now; routing the wire through it is a separate, unmeasured change | If a future consumer reads the Stationary* bits expecting retail's handle_all_collisions to have set them (it doesn't run in Core), the Core writeback is the source of truth; a wire-reporting change that assumes CachedVelocity is live would send the wrong velocity until it's wired | `handle_all_collisions` bit encode pc:282737-758; `get_velocity` 0x005113c0 (cached_velocity reader) |
|
||||
| AD-41 | The `candidateMoved` gate (retail UpdateObjectInternal pc:283657 `candidate != m_position`) suppresses ONLY `handle_all_collisions` + `cached_velocity` on a no-move frame; acdream still runs `ResolveWithTransition` (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) | `src/AcDream.App/Input/PlayerMovementController.cs` (`candidateMoved` guard) | The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh | If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 pc:283657 (candidate-moved gate) |
|
||||
|
|
|
|||
|
|
@ -593,7 +593,7 @@ Research: R9 + R12 + R13.
|
|||
- **✓ SHIPPED — G.1 — Sky + weather + day-night.** Deterministic client-side from Portal Year time. Sky dome geometry + keyframe gradients + rain/snow particles. See `r12-weather-daynight.md`. Full data + visual stack shipped: Region dat loader, keyframe interp, WeatherSystem with 5-kind PDF + transitions + storm flashes, WorldSession→WorldTimeService sync via ConnectRequest+TimeSync, SkyRenderer with sky-object arcs + UV scroll, rain/snow billboard renderer, F7/F10 debug cycle keys.
|
||||
- **✓ SHIPPED — G.2 — Dynamic lighting.** 8-light D3D-style fixed pipeline. Hard-cutoff at Range, no attenuation inside. Cell ambient. Shader UBO per frame. See `r13-dynamic-lighting.md`. SceneLightingUbo std140 at binding=1 feeds terrain + mesh + mesh_instanced + sky shaders. LightingHookSink auto-registers Setup.Lights at entity stream-in, flips IsLit on SetLightHook, unregisters on landblock unload.
|
||||
- **Indoor portal-based cell tracking (follow-up to Indoor walking Phase 1 / issue #87).** Replace `PhysicsDataCache.TryFindContainingCell` AABB containment with retail's `CObjMaint::HandleObjectEnterCell` portal traversal. When the player crosses a cell portal boundary, `CellId` propagates through the `CEnvCell` portal connectivity graph. Prerequisite for wall collision from outside (#85) and the remaining #84 threshold symptom. PDB symbols and `acclient.h` `CCellStructure` refs are in place (see #87). **Unblocks G.3.**
|
||||
- **✓ SHIPPED — G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with post-transition `LoginComplete`, and the retail DAT-authored portal-space CreatureMode viewport (Setup `0x02000306`, animation `0x030005AC`, exact camera/light/timing/fades; visual gate pending 2026-07-15). Dungeons render, stream, teleport-in, collide, light, and their doors work — see the shipped-table rows (G.3, G.3a, #137 collision, A7.L1 lighting) below and the M1.5 section of `docs/plans/2026-05-12-milestones.md` for current gate status (one recorded end-to-end round-trip user gate outstanding; live residual = far-town teleport-OUT arrival cascade, #145-residual REOPENED). See `r09-dungeon-portal-space.md` and `docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||||
- **✓ SHIPPED — G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with post-transition `LoginComplete`, and the retail DAT-authored portal-space CreatureMode viewport (Setup `0x02000306`, animation `0x030005AC`, exact camera/light/timing/fades, forward-axis roll, projection warp, and destination viewer reset; corrective visual gate pending 2026-07-15). Dungeons render, stream, teleport-in, collide, light, and their doors work — see the shipped-table rows (G.3, G.3a, #137 collision, A7.L1 lighting) below and the M1.5 section of `docs/plans/2026-05-12-milestones.md` for current gate status (one recorded end-to-end round-trip user gate outstanding; live residual = far-town teleport-OUT arrival cascade, #145-residual REOPENED). See `r09-dungeon-portal-space.md` and `docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||||
|
||||
**Acceptance:** walk outside at dusk, see the sky gradient + sun moving; enter a torch-lit dungeon via portal; leave back to daylight.
|
||||
|
||||
|
|
|
|||
|
|
@ -305,8 +305,9 @@ capture-harness-first).
|
|||
temporary black tunnel cover is retired. F751 transit now renders retail's
|
||||
client-enum-resolved DAT Setup `0x02000306` with animation `0x030005AC` at
|
||||
40 frames/s in a dedicated CreatureMode-equivalent viewport, using retail's
|
||||
camera, distant light, random eased yaw, frame-aligned exit window, and exact
|
||||
100-sample fade curve. The viewport and fades compose below retained UI, so
|
||||
camera, distant light, random eased forward-axis roll, frame-aligned exit
|
||||
window, exact 100-sample fade curve, view-plane/FOV warp, and destination
|
||||
viewer reset. The viewport and fades compose below retained UI, so
|
||||
chat, panels, toolbar, cursor, and input remain live throughout travel. See
|
||||
`docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Named Sept-2013 retail symbols:
|
|||
- `gmSmartBoxUI::UseTime` `0x004D6E30`
|
||||
- `CreatureMode::CreatureMode` `0x00454390`
|
||||
- `CreatureMode::SetCameraDirection_Degrees` `0x00453760`
|
||||
- `Frame::rotate` `0x004525B0`
|
||||
- `CreatureMode::AddObject` `0x004556D0`
|
||||
- `UIGlobals::Init` `0x004EE470`
|
||||
- `UIGlobals::GetAnimLevel` `0x004EE540`
|
||||
|
|
@ -24,6 +25,9 @@ Named Sept-2013 retail symbols:
|
|||
- `SmartBox::PlayerPositionUpdated` `0x00453870`
|
||||
- `CPhysicsObj::teleport_hook` `0x00514ED0`
|
||||
- `CommandInterpreter::PlayerTeleported` `0x006B32B0`
|
||||
- `SmartBox::SetOverrideFovDistance` `0x00451BC0`
|
||||
- `SmartBox::GetOverrideFovDistance` `0x00451BE0`
|
||||
- `Render::set_vdst` `0x0054B240`
|
||||
|
||||
Constants were checked against static x86 disassembly. The Binary Ninja
|
||||
pseudocode loses several x87 operands in `UseTime`; disassembly is authoritative
|
||||
|
|
@ -93,7 +97,12 @@ sequence animations, plays `Sound_UI_ExitPortal`, and enters `WorldFadeIn`.
|
|||
|
||||
## Camera motion
|
||||
|
||||
The camera rotates around AC's vertical `+Z` axis. Each segment is independent:
|
||||
The camera rolls around its own AC `+Y` forward axis. It does not yaw around
|
||||
world `+Z`. `SetCameraDirection_Degrees` resets the frame to identity, converts
|
||||
the vector to radians, then passes `(0, angle, 0)` to `Frame::rotate`, whose
|
||||
input is an axis-angle rotation vector. The forward direction therefore stays
|
||||
down the portal passage while local `+Z` up rolls around it. Each segment is
|
||||
independent:
|
||||
|
||||
```text
|
||||
if now >= rotationStart + rotationDuration:
|
||||
|
|
@ -184,6 +193,39 @@ World/tunnel fades use `GetAnimLevel(elapsed / FadeTime)`. The fade belongs
|
|||
between the 3-D viewport and the retained UI. It must never cover or disable
|
||||
the retained UI.
|
||||
|
||||
## View-plane warp
|
||||
|
||||
Retail couples every fade state to a projection transition. On begin it
|
||||
captures the current SmartBox view-plane distance; for a perspective
|
||||
projection this is `cot(verticalFov / 2)`. The transition endpoint is the
|
||||
named constant `TRANSITION_VIEW_PLANE_DISTANCE = 0.001` at `0x007BD260`.
|
||||
|
||||
```text
|
||||
fadeLevel = GetAnimLevel(elapsed / FadeTime) / 1024
|
||||
|
||||
WorldFadeOut or TunnelFadeOut:
|
||||
currentDistance = gameDistance
|
||||
+ (0.001 - gameDistance) * fadeLevel
|
||||
|
||||
TunnelFadeIn or WorldFadeIn:
|
||||
currentDistance = 0.001
|
||||
+ (gameDistance - 0.001) * fadeLevel
|
||||
|
||||
SmartBox.SetOverrideFovDistance(true, currentDistance)
|
||||
```
|
||||
|
||||
`Render::set_vdst` turns the distance back into projection values:
|
||||
|
||||
```text
|
||||
verticalFov = 2 * atan(1 / currentDistance)
|
||||
znear = max(0.1, currentDistance * 0.25)
|
||||
```
|
||||
|
||||
At `0.001`, the view is nearly 180 degrees wide. Black covers the singular
|
||||
endpoint; as `WorldFadeIn` clears, the FOV eases back to the captured game
|
||||
projection. This is retail's characteristic destination-world warp, separate
|
||||
from both the alpha fade and chase-camera movement.
|
||||
|
||||
## Player placement boundary
|
||||
|
||||
```text
|
||||
|
|
@ -195,7 +237,7 @@ PlayerPositionUpdated(isTeleport=true):
|
|||
clear position-update/teleport bookkeeping
|
||||
player.teleport_hook()
|
||||
commandInterpreter.PlayerTeleported()
|
||||
move world viewer to player
|
||||
SmartBox.set_viewer(player.position, reset_sought=true)
|
||||
update CellManager position
|
||||
|
||||
player.teleport_hook():
|
||||
|
|
@ -212,6 +254,11 @@ PlayerTeleported():
|
|||
SendMovementEvent()
|
||||
```
|
||||
|
||||
The viewer reset copies the player's complete position into both `viewer` and
|
||||
`viewer_sought_position`. Subsequent ordinary camera updates extend the chase
|
||||
boom outward through the same damped and swept path used after camera
|
||||
collision. Reusing the source-world viewer is not retail behavior.
|
||||
|
||||
The retail teleport hook does not clear the character's animation sequence.
|
||||
The arrival presentation is instead hidden by the portal scene and its final
|
||||
world fade while the normal motion/update stream settles. Therefore acdream
|
||||
|
|
@ -227,6 +274,11 @@ must not add an arrival-only animation reset.
|
|||
- During tunnel states the portal scene replaces world pixels before retained
|
||||
UI draws. The black fade also draws before retained UI. Input dispatch and
|
||||
`RetailUiRuntime.Tick` continue unchanged.
|
||||
- The projection owner captures the active camera's M22 view-plane distance at
|
||||
teleport begin and applies `Render::set_vdst` semantics to both the tunnel
|
||||
and world viewport during the four fade states.
|
||||
- Destination placement resets the retail chase camera's published and sought
|
||||
positions to the player before the normal update path resumes.
|
||||
- Destination residency remains acdream's asynchronous adaptation. It supplies
|
||||
the same `EndTeleportAnimation` edge retail receives when its blocking cell
|
||||
load completes; it does not alter presentation ordering.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue