docs(motion): #42 root cause confirmed — ResolveWithTransition airborne drift
A/B-tested 2026-05-05 with user observing retail-controlled remote:
- With CellId fix removed: jumps render with geometrically-correct
XY (no drift) but body falls through the floor.
- With CellId fix applied: jumps land cleanly but arc shows ~1 m
horizontal offset; snaps back on next UM.
Confirms the drift originates inside ResolveWithTransition, not from
wire data, local Euler error, or stale velocity. CellId fix kept in
place because falling through the floor is more disruptive than
~1 m visual jitter that resolves on next input.
#42 updated with the verified diagnosis, three ranked-by-probability
hypotheses for the in-sweep mechanism (initial-overlap depenetration
along non-+Z terrain normal is the leading candidate), three matching
fix paths, and a deterministic repro recipe for the next session.
The right next step is investigating PhysicsEngine.ResolveWithTransition
and comparing against retail's CTransition::find_valid_position
(docs/research/named-retail/) — out of scope for the L.3 motion port,
files as a follow-up PhysicsEngine bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c26bbbb84e
commit
b37b7137f6
2 changed files with 97 additions and 44 deletions
133
docs/ISSUES.md
133
docs/ISSUES.md
|
|
@ -127,9 +127,30 @@ the same direction. Add a `LastUMUpdateTime` grace window (e.g.
|
||||||
## #42 — Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
|
## #42 — Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
**Severity:** MEDIUM (pre-existing; exposed by L.3 M2 airborne UP no-op)
|
**Severity:** MEDIUM (pre-existing PhysicsEngine bug; exposed by L.3 M2 airborne UP no-op + M4 CellId fix)
|
||||||
**Filed:** 2026-05-05
|
**Filed:** 2026-05-05 (root cause confirmed same day)
|
||||||
**Component:** physics / motion (airborne local-integration)
|
**Component:** physics (`PhysicsEngine.ResolveWithTransition` airborne behavior)
|
||||||
|
|
||||||
|
**Root cause (verified 2026-05-05 via A/B test):**
|
||||||
|
|
||||||
|
`ResolveWithTransition` running per-tick during the airborne arc is the
|
||||||
|
source of the drift. Verified by A/B-toggling the M4 CellId fix
|
||||||
|
(`rmState.CellId = p.LandblockId`) which is the gate that lets the
|
||||||
|
sweep run for player-remote jumps:
|
||||||
|
|
||||||
|
- **CellId line removed** → sweep skipped → jumps render with
|
||||||
|
geometrically-correct XY (no drift) but body falls through the
|
||||||
|
floor (no terrain catch).
|
||||||
|
- **CellId line present** → sweep runs → jumps land correctly but
|
||||||
|
arc shows ~1 m horizontal offset from actor's actual XY; body
|
||||||
|
snaps back on next inbound UM.
|
||||||
|
|
||||||
|
So the drift originates inside `ResolveWithTransition` itself, not
|
||||||
|
from wire data, not from local Euler integration, not from stale
|
||||||
|
velocity. Decision recorded in commit history: kept CellId fix in
|
||||||
|
production code so jumps land (`fall-through-floor` is more disruptive
|
||||||
|
to gameplay than `~1m visual jitter that resolves on next input`).
|
||||||
|
This issue tracks the proper fix.
|
||||||
|
|
||||||
**Description:** When observing a retail-controlled remote that jumps
|
**Description:** When observing a retail-controlled remote that jumps
|
||||||
in place (no horizontal input), the visible jump arc renders with
|
in place (no horizontal input), the visible jump arc renders with
|
||||||
|
|
@ -175,57 +196,91 @@ accumulated local-integration drift. The drift is pre-existing — the
|
||||||
user reports having seen it before — but is now visible for the
|
user reports having seen it before — but is now visible for the
|
||||||
full arc duration instead of being corrected every ~200 ms.
|
full arc duration instead of being corrected every ~200 ms.
|
||||||
|
|
||||||
**Suspected sources of XY drift on a stationary jump:**
|
**Likely mechanism (ranked by probability):**
|
||||||
|
|
||||||
1. **ACE wire VectorUpdate may have non-trivial XY components** even
|
1. **Initial-overlap depenetration along non-+Z terrain normal** — at
|
||||||
when the actor is standing still. `OnLiveVectorUpdated` (line
|
jump start the collision sphere is touching the floor at body Z.
|
||||||
3235) sets `rm.Body.Velocity = update.Velocity` verbatim; no
|
Most outdoor terrain triangles are not perfectly horizontal — their
|
||||||
filtering. Worth instrumenting `[VU.WIRE]` to confirm wire XY for
|
normals have a small horizontal component. The sweep's first action
|
||||||
stationary jumps.
|
each tick is to resolve overlap by separating the sphere along the
|
||||||
|
contact normal; on a tilted terrain triangle that separation has
|
||||||
|
horizontal magnitude. The body gets shoved sideways the first frame
|
||||||
|
of the jump and the rest of the arc carries that initial drift.
|
||||||
|
Direction-correlation with terrain orientation would confirm
|
||||||
|
(test in different landblocks; if drift direction varies with the
|
||||||
|
slope of the launch tile, this is it).
|
||||||
|
|
||||||
2. **Pre-jump `rm.Body.Velocity` residual** — should be zero for the
|
2. **Step-down probe firing despite `isOnGround: false`** — sweep's
|
||||||
M3 grounded path (cleared each tick at line 6118 area), but worth
|
internal "search for nearest walkable surface" might still scan
|
||||||
confirming via diag.
|
horizontally during airborne ticks even when we pass `isOnGround:
|
||||||
|
!rm.Airborne` (= false for airborne). Check whether the
|
||||||
|
`stepUpHeight` / `stepDownHeight` parameters are unconditionally
|
||||||
|
used inside `ResolveWithTransition` regardless of the
|
||||||
|
`isOnGround` flag.
|
||||||
|
|
||||||
3. **EdgeSlide during sphere sweep** — if `ResolveWithTransition`
|
3. **EdgeSlide on near-vertical motion against a near-vertical
|
||||||
catches an edge mid-arc (unlikely for a stationary in-place jump
|
surface** — if the sphere even slightly grazes a wall while
|
||||||
but possible), the sweep could push the body horizontally.
|
ascending or descending, EdgeSlide projects motion tangent to the
|
||||||
|
wall, redirecting some Z velocity into XY. Less likely for
|
||||||
4. **Render-rate-dependent dt** — local Euler integration uses
|
open-ground stationary jumps but could explain drift near
|
||||||
`Silk.NET.OnRender(double deltaSeconds)` raw; retail clamps to
|
buildings.
|
||||||
30 Hz. Sub-tick error accumulates over a 2 s arc.
|
|
||||||
|
|
||||||
**Fix paths:**
|
**Fix paths:**
|
||||||
|
|
||||||
a. **Pragmatic** (revert to legacy behavior): hard-snap
|
a. **Skip initial-overlap depenetration when airborne** — gate the
|
||||||
`rm.Body.Position = worldPos` on airborne UPs too. Diverges from
|
"separate from initial contact plane" step inside
|
||||||
retail spec, but ACE behavior diverges from retail too (ACE
|
`ResolveWithTransition` on `isOnGround: true`. Trusts the previous
|
||||||
broadcasts mid-arc UPs while retail apparently doesn't). Masks
|
tick's resolve to have left the body in a non-overlapping position.
|
||||||
the drift identically to pre-M2. Lowest-risk visual fix.
|
This is the most likely-correct fix if hypothesis (1) is right.
|
||||||
|
|
||||||
b. **Investigate** the actual XY source via VU/UP instrumentation
|
b. **Zero step-up/down for airborne sweeps** — pass
|
||||||
and `body.Velocity` snapshots, then fix the root cause. May be
|
`stepUpHeight: 0f, stepDownHeight: 0f` when `rm.Airborne`. Kills
|
||||||
an ACE-specific velocity-quirk we should clamp at
|
hypothesis (2) without other side effects (airborne bodies don't
|
||||||
`OnLiveVectorUpdated`, or a clock-source mismatch in our Euler.
|
step anyway).
|
||||||
|
|
||||||
c. **Hybrid**: keep airborne no-op for body.Position translation but
|
c. **Stripped airborne sweep** — replace the full sphere sweep with
|
||||||
re-introduce a soft-correction on mid-arc UPs (server-position-
|
a simpler vertical sphere-vs-terrain intersection + wall-collision
|
||||||
biased lerp) — slowly pulls body toward server truth without
|
stop. Loses some retail fidelity but eliminates all three
|
||||||
the rubber-band.
|
mechanisms. Probably overkill if (a) or (b) suffices.
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveVectorUpdated`
|
- `src/AcDream.Core/Physics/PhysicsEngine.cs` —
|
||||||
L3228+ (sets velocity from wire verbatim)
|
`ResolveWithTransition` and any internal `CTransition` /
|
||||||
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLivePositionUpdated`
|
`find_valid_position` helpers. The initial-overlap depenetration
|
||||||
player-remote airborne no-op L3502+ (the no-op that exposed this)
|
path is the primary investigation target.
|
||||||
- `src/AcDream.App/Rendering/GameWindow.cs` legacy airborne TickAnimations
|
- `src/AcDream.App/Rendering/GameWindow.cs:6478+` (legacy airborne
|
||||||
L6478+ (gravity integration via UpdatePhysicsInternal)
|
TickAnimations, the call site) — reference only; not the bug.
|
||||||
|
|
||||||
|
**Reference:**
|
||||||
|
|
||||||
|
Retail equivalent at
|
||||||
|
`docs/research/named-retail/acclient_2013_pseudo_c.txt`:
|
||||||
|
- `CTransition::find_valid_position` (called from `transition()`)
|
||||||
|
- `SpherePath` initialization
|
||||||
|
- The verbatim retail depenetration logic for airborne bodies
|
||||||
|
|
||||||
|
If our port differs from retail in this region, that diff is likely
|
||||||
|
the bug.
|
||||||
|
|
||||||
|
**Repro:**
|
||||||
|
|
||||||
|
1. Launch acdream + retail client side-by-side connected to local ACE.
|
||||||
|
2. Have retail char stand still on outdoor terrain at any position X.
|
||||||
|
3. Jump in place.
|
||||||
|
4. Observe acdream window: arc renders ~1 m offset from X, lands
|
||||||
|
offset, snaps back on next UM.
|
||||||
|
|
||||||
|
To verify the depenetration hypothesis specifically, repeat the jump
|
||||||
|
in different landblock spots — drift direction should correlate with
|
||||||
|
the local terrain normal, not the actor's facing.
|
||||||
|
|
||||||
**Acceptance:**
|
**Acceptance:**
|
||||||
|
|
||||||
- Visual jump arc + landing render at the actor's actual XY position,
|
- Visual jump arc + landing render at the actor's actual XY position,
|
||||||
no perceptible horizontal offset, no snap-back on next UM/UP.
|
no perceptible horizontal offset, no snap-back on next UM.
|
||||||
|
- Wall-collision airborne (jumping into building doorways, jumping
|
||||||
|
puzzles) still works — fix must not strip collision wholesale.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3434,12 +3434,10 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
// Adopt server's cell ID on every UP (airborne or grounded).
|
// Adopt server's cell ID on every UP (airborne or grounded).
|
||||||
// Required by the legacy airborne path's per-tick
|
// Required by the legacy airborne path's per-tick
|
||||||
// ResolveWithTransition gate (rm.CellId != 0) — without this,
|
// ResolveWithTransition gate (rm.CellId != 0); without this
|
||||||
// an airborne player remote falls through the floor because
|
// an airborne player remote falls through the floor because
|
||||||
// the sphere sweep is skipped, K-fix15 landing detection never
|
// the sphere sweep is skipped. Note: enabling the sweep also
|
||||||
// fires, and the body only re-grounds when the next UM forces
|
// exposes a pre-existing depenetration bug — see #42.
|
||||||
// ACE to broadcast a fresh IsGrounded=true UP that hits our
|
|
||||||
// landing transition branch below.
|
|
||||||
rmState.CellId = p.LandblockId;
|
rmState.CellId = p.LandblockId;
|
||||||
|
|
||||||
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous
|
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue