fix(runtime): align portal and movement presentation

Port retail portal viewport projection and reveal behavior, preserve outbound combat style, drive remote and local grounded movement from authored CSequence root frames, and reuse the local prepared pose so animation hooks advance once.

User-verified portal, observer movement, combat stance, and short-tap locomotion gates. Release build passed with 5,767 tests and five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 08:48:27 +02:00
parent e95f55f25b
commit 124e046976
30 changed files with 1362 additions and 348 deletions

View file

@ -44,6 +44,78 @@ Copy this block when adding a new issue:
---
## #220 — Local player glides before locomotion animation starts
**Status:** DONE 2026-07-17 — user-confirmed connected visual gate
**Severity:** HIGH
**Filed:** 2026-07-17
**Component:** local movement / animation / physics
**Description:** A very short W tap moved the acdream character before the
Ready→Walk/Run animation visibly started, producing a brief foot-slide.
**Root cause / status:** The grounded local controller called
`CMotionInterp::get_state_velocity` and installed the full 3.124.0 m/s body
velocity on the input edge. Retail instead advances `CPartArray` first and
uses the exact root displacement authored by the current transition/cycle;
`PositionManager::adjust_offset` then composes into that same Frame before the
collision sweep. The local player now advances its owning `AnimationSequencer`
after input dispatch, accumulates the emitted root displacement to the 30 Hz
physics quantum, and passes it through the existing PositionManager/collision
path. The prepared part pose is reused by the presentation pass so animation
time and hooks advance exactly once.
**Files:** `src/AcDream.App/Input/PlayerMovementController.cs`;
`src/AcDream.App/Rendering/GameWindow.cs`;
`tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs`;
`tests/AcDream.Core.Tests/Physics/HumanoidMotionTableRootMotionTests.cs`.
**Research:**
`docs/research/2026-07-17-local-player-root-motion-start-pseudocode.md`.
**Acceptance:** From rest, tap W briefly several times. The body must not move
before the Ready→Walk/Run transition produces movement. Holding W must still
reach normal continuous speed; release, backward/strafe, and jump remain
correct.
Passed 2026-07-17: “Good! works.”
---
## #219 — Outbound movement dropped the player's combat stance
**Status:** DONE 2026-07-17 — user-confirmed from a retail observer
**Severity:** HIGH
**Filed:** 2026-07-17
**Component:** movement wire / combat stance / remote animation
**Description:** acdream looked correct locally while moving in combat mode,
but a retail observer saw the character leave combat stance and locomote in
NonCombat.
**Root cause / status:** Retail `CommandInterpreter::SendMovementEvent` passes
the canonical `MovementManager::RawMotionState` directly into
`MoveToStatePack`. acdream instead projected the raw axes through
`MovementResult` and rebuilt a packet state without `CurrentStyle`, leaving its
retail default `0x8000003D` NonCombat. `RawMotionStatePacker` therefore omitted
the style bit; ACE relayed no combat stance, and retail's unpack correctly
defaulted the observer to NonCombat. `MovementResult` now carries the canonical
`MotionInterpreter.RawState.CurrentStyle`, and the outbound builder writes it
without inferring from CombatMode or equipment.
**Files:** `src/AcDream.App/Input/PlayerMovementController.cs`;
`src/AcDream.App/Input/LocalPlayerOutboundController.cs`;
`tests/AcDream.App.Tests/Input/LocalPlayerOutboundCombatStyleTests.cs`.
**Research:**
`docs/research/2026-07-17-combat-movement-outbound-style-pseudocode.md`.
**Acceptance:** With a melee, bow/crossbow, or magic stance active, move the
acdream player while observing from retail. The observer retains the same
combat stance and uses its combat locomotion instead of switching to peace.
Passed 2026-07-17: “Yes works.”
---
## #218 — Portal silhouette pose, destination reveal, and indoor observer snap
**Status:** IN-PROGRESS — root fixes implemented 2026-07-16; pending connected visual gate
@ -93,7 +165,10 @@ removed by owner before prefix cleanup, including footprints flooded across a
landblock seam, while server-live/dynamic registrations remain refloodable.
Full unload uses the same owner rule; surviving owners seeded across the seam
track withdrawn prefixes so a later reload restores their missing collision
rows.
rows. Destination reveal now also keeps sky and landscape on the same active
SmartBox projection, matching `SmartBox::RenderNormalMode`/`GameSky::Draw`;
the old fixed 60-degree sky camera exposed the clear/fog background during the
nearly 180-degree exit warp.
**Files:** `src/AcDream.Core/Physics/AnimationSequencer.cs`;
`src/AcDream.App/Rendering/GameWindow.cs`;
@ -101,6 +176,7 @@ rows.
`src/AcDream.App/Streaming/GpuWorldState.cs`;
`src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`;
`src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`;
`src/AcDream.App/Rendering/Sky/SkyProjection.cs`;
`src/AcDream.Core/World/ProceduralSceneryIdAllocator.cs`;
`src/AcDream.Core/Physics/PhysicsBody.cs`;
`src/AcDream.App/Input/PlayerMovementController.cs`.
@ -4802,85 +4878,49 @@ the local terrain normal, not the actor's facing.
## #41 — Residual sub-decimeter blips on observed player remotes (M3 baseline)
**Status:** OPEN
**Severity:** LOW (within retail's own DesiredDistance / MinDistance tolerances; visible only on close inspection)
**Status:** FIX IMPLEMENTED 2026-07-17 — pending acdream-observer visual gate
**Severity:** MEDIUM
**Filed:** 2026-05-05
**Component:** physics / motion / animation (per-tick remote prediction)
**Phase:** L.2 (Movement & Collision Conformance) — inbound-motion fidelity sub-piece. Blocked on cdb-trace of `CSequence::velocity` for Humanoid running cycle, then porting `add_motion @ 0x005224b0`'s `style_speed × MotionData.velocity` chain.
**Phase:** L.2 (Movement & Collision Conformance) — inbound-motion fidelity sub-piece.
**Description:** With the L.3 M3 path live (queue catch-up + animation
root motion fallback), observed player remotes chase server position
smoothly with NO staircase on slopes and NO per-UP rubber-band. However
small position blips remain — sub-decimeter amplitude, periodic with
the server's UP cadence (~1 Hz). User report 2026-05-05: "I get very
small blips now. Running works, walking works, strafing works."
**Description:** Observed characters walked forward, blipped backward, then
continued, periodic with the server's UpdatePosition cadence. The earlier
2026-05-05 report called the residue sub-decimeter; on 2026-07-17 the user
reported the same mechanism as plainly visible repeated rollback.
The blips fall well within retail's own tolerances:
**Root cause / fix:** Retail `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`
advances `CPartArray::Update` into a complete local root-motion Frame, then
lets `PositionManager::adjust_offset` replace that Frame with interpolation
catch-up. `add_motion @ 0x005224B0` writes only literal
`MotionData.Velocity × speed` into CSequence. acdream instead synthesized
Walk/Run constants into `AnimationSequencer.CurrentVelocity` and reconstructed
a delta after the queue emptied. The installed Humanoid MotionTable proves
both Walk and Run carry zero MotionData velocity, so acdream ran past the last
server waypoint; the next UpdatePosition pulled it backward.
- `DesiredDistance` (queue head reach radius) = 0.05 m
- `MinDistanceToReachPosition` (primary stall threshold) = 0.20 m
So they are NOT a stall trigger and NOT a correctness bug. They're a
visible artifact of the velocity-synthesis residual: anim root motion
(`AnimationSequencer.CurrentVelocity = RunAnimSpeed × adjustedSpeed`)
slightly overshoots server pace between UPs, then queue catch-up walks
the body back toward the server position on the next UP — a small
rubber-band that's smaller than M2's pre-fix version but still
perceptible.
**Root cause hypothesis (untested):**
The L.3 handoff explicitly flagged this. From `06-acdream-audit.md` § 9
and `05-position-manager-and-partarray.md` § 7:
> Our `CurrentVelocity` carries only the steady-state component of the
> cycle's intent; the per-frame stride wobble is gone… For Humanoid
> the dat ships `MotionData.Velocity = 0` so the multiply is a no-op
> anyway — but the synth uses `RunAnimSpeed × adjustedSpeed` directly.
ACE's wire `ForwardSpeed` for a running player is the **server runRate**
(~2.94 for skill 200), not a unit multiplier. Our synth multiplies
`RunAnimSpeed` (4.0) by `adjustedSpeed` (~2.94) = ~11.76 m/s, which
the queue catch-up clamps via `min(catchUp × dt, dist)` but the anim
fallback applies in full when the queue is idle. If the actual
server-broadcast pace is closer to 4.0 m/s (RunAnimSpeed alone, with
runRate as a *frame-rate* multiplier rather than a velocity scalar),
our fallback overshoots by ~3× and the queue walks it back every UP.
Per the handoff: **don't normalize at the wire boundary** (prior
session tried this, called it a hack). The right fix is porting
retail's actual behavior in `add_motion @ 0x005224b0` and
`apply_run_to_command` to determine the correct `CSequence::velocity`
magnitude.
Remote animation now advances before remote physics into the already-ported
CSequence root-motion Frame. `RemotePhysicsUpdater` consumes that exact local
delta, and `RemoteMotionCombiner` applies the retail interpolation-replaces-
root-motion rule. Command-derived locomotion synthesis was removed from
CSequence; the local interpreted body path retains retail
`CMotionInterp::get_state_velocity` through its existing zero-data fallback.
**Files:**
- `src/AcDream.Core/Physics/AnimationSequencer.cs``CurrentVelocity`
synthesis at L614679 (RunAnimSpeed=4.0, WalkAnimSpeed=3.12,
SidestepAnimSpeed=1.25 × adjustedSpeed)
- `src/AcDream.Core/Physics/PositionManager.cs` — `ComputeOffset`
applies `seqVel × dt × orientation` as fallback when queue is idle
- `src/AcDream.Core/Physics/AnimationSequencer.cs`
- `src/AcDream.Core/Physics/RemoteMotionCombiner.cs`
- `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`
- `src/AcDream.App/Rendering/GameWindow.cs`
- `tests/AcDream.Core.Tests/Physics/HumanoidMotionTableRootMotionTests.cs`
**Research:**
- `docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md` § 57
- `docs/research/2026-05-04-l3-port/06-acdream-audit.md` § 9 (AnimationSequencer)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` line 298437
(`add_motion @ 0x005224b0`) — `CSequence::velocity = style_speed × MotionData.velocity`
**Fix path (research first, then port):**
1. cdb-trace retail to capture `CSequence::velocity` and
`MotionData::velocity` for a Humanoid running cycle. Compare against
our synth (4.0 × 2.94 = 11.76 m/s) to determine the actual retail
magnitude.
2. Port `add_motion`'s `style_speed × MotionData.velocity` chain
verbatim. For Humanoid where `MotionData.Velocity = 0`, port the
fallback retail uses (likely a separate code path through
`apply_run_to_command` that derives velocity from the cycle's
framerate, not a constant).
3. Remove the `RunAnimSpeed × adjustedSpeed` synth in
`AnimationSequencer.SetCycle`.
- `docs/research/2026-07-17-remote-root-motion-reconciliation-pseudocode.md`
- `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md`
- named retail `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`,
`CPartArray::Update @ 0x00517DB0`, `add_motion @ 0x005224B0`, and
`InterpolationManager::adjust_offset @ 0x00555D30`
**Acceptance:**
@ -4888,8 +4928,6 @@ magnitude.
- Side-by-side acdream-as-observer vs retail-as-observer of the same
server-controlled toon: indistinguishable body trajectory.
**2026-07-09 note (user):** "semi fixed, needs polish" — stays open, downgrade to polish-tier if a fresher look confirms the core bug is gone.
---
## #40 — [DONE 2026-05-05 · 40d88b9] ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
@ -7264,7 +7302,7 @@ The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5) inst
## #113 — Phantom staircase / holistic building-render port
**Status:** DONE (2026-07-09, user-confirmed via memory during an issues-triage pass — not independently re-verified this session).
The Holtburg meeting-hall's walkable stair-ramp painted across the exterior wall because the PView shell-clip pass computed correct clip regions but never enabled `GL_CLIP_DISTANCE` — fixed by `927fd8f`. The bug reopened 2026-06-11 when a second root cause surfaced (dictionary-referenced-but-undrawn BSP polys drawn that retail never draws); per the user's mandate this was folded into the holistic building-render port effort (#113/#114/#108/#109/#99, charter at `docs/research/2026-06-11-building-render-holistic-port-handoff.md`), which shipped as part of the broader render-pipeline redesign (Option A) closed 2026-06-12.
The Holtburg meeting-hall's walkable stair-ramp painted across the exterior wall because the PView shell-clip pass computed correct clip regions but never enabled `GL_CLIP_DISTANCE` — fixed by `927fd8f`. The bug reopened 2026-06-11 when a suspected second root cause surfaced (dictionary-referenced-but-undrawn BSP polys); per the user's mandate this was folded into the holistic building-render port effort (#113/#114/#108/#109/#99, charter at `docs/research/2026-06-11-building-render-holistic-port-handoff.md`), which shipped as part of the broader render-pipeline redesign (Option A) closed 2026-06-12. A 2026-07-16 named-retail audit later disproved the proposed global DrawingBSP polygon filter: `CGfxObj::InitLoad`/`D3DPolyRender::ConstructMesh` consume the complete polygon array, and the alleged shell orphans are portal references omitted by the old diagnostic collector. See retired divergence TS-20; do not retry `e46d3d9`'s filter, which made doors disappear.
## #111 — ACE-mutated indoor restores: transparent interior / wrong placement at login — [DONE 2026-06-10 · 5f1eb7c + 5706e0e + 2735695]
@ -7841,18 +7879,18 @@ formula.
**Resolution:** Grounded player remotes showed a ~5 Hz Z staircase when
running up/down hills. `PositionManager.ComputeOffset` has two modes:
queue-active (3D direction toward server's broadcast position, Z
follows naturally) and queue-empty / head-reached (`seqVel × dt`
rotated into world). Every locomotion cycle bakes Z=0 in body-local,
follows naturally) and queue-empty / head-reached (the CSequence
root-motion delta rotated into world). Every locomotion cycle bakes Z=0 in body-local,
so the world result has Z=0 too. With server UPs at ~5 Hz and
catchUpSpeed = 2× maxSpeed, body chases each waypoint in ~100ms (Z
ramps), then sits in seqVel-only mode for ~100ms (Z flat) until the
ramps), then sits in root-motion-only mode for ~100ms (Z flat) until the
next UP. Visible 5 Hz staircase.
Fix mirrors retail's `CTransition::adjust_offset` contact-plane
projection (named-retail acclient_2013_pseudo_c.txt:272296-272346),
applied at the queue-empty boundary instead of inside the sweep.
`ComputeOffset` gains an optional `Vector3? terrainNormal`; when
the seqVel fallback runs and the supplied normal is non-trivial,
the root-motion fallback runs and the supplied normal is non-trivial,
`rootMotionWorld -= N × dot(rootMotionWorld, N)`. XY motion gains a
Z component proportional to slope × forward speed; body Z follows the
terrain mesh between UPs. No-op on flat ground (N ≈ +Z, dot ≈ 0) so