From b37b7137f6d7008e57b893ed8b3c4d2617afe9fc Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 5 May 2026 15:47:40 +0200 Subject: [PATCH] =?UTF-8?q?docs(motion):=20#42=20root=20cause=20confirmed?= =?UTF-8?q?=20=E2=80=94=20ResolveWithTransition=20airborne=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/ISSUES.md | 133 +++++++++++++++++------- src/AcDream.App/Rendering/GameWindow.cs | 8 +- 2 files changed, 97 insertions(+), 44 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index b1cc364..96fcfba 100644 --- a/docs/ISSUES.md +++ b/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) **Status:** OPEN -**Severity:** MEDIUM (pre-existing; exposed by L.3 M2 airborne UP no-op) -**Filed:** 2026-05-05 -**Component:** physics / motion (airborne local-integration) +**Severity:** MEDIUM (pre-existing PhysicsEngine bug; exposed by L.3 M2 airborne UP no-op + M4 CellId fix) +**Filed:** 2026-05-05 (root cause confirmed same day) +**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 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 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 - when the actor is standing still. `OnLiveVectorUpdated` (line - 3235) sets `rm.Body.Velocity = update.Velocity` verbatim; no - filtering. Worth instrumenting `[VU.WIRE]` to confirm wire XY for - stationary jumps. +1. **Initial-overlap depenetration along non-+Z terrain normal** — at + jump start the collision sphere is touching the floor at body Z. + Most outdoor terrain triangles are not perfectly horizontal — their + normals have a small horizontal component. The sweep's first action + 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 - M3 grounded path (cleared each tick at line 6118 area), but worth - confirming via diag. +2. **Step-down probe firing despite `isOnGround: false`** — sweep's + internal "search for nearest walkable surface" might still scan + 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` - catches an edge mid-arc (unlikely for a stationary in-place jump - but possible), the sweep could push the body horizontally. - -4. **Render-rate-dependent dt** — local Euler integration uses - `Silk.NET.OnRender(double deltaSeconds)` raw; retail clamps to - 30 Hz. Sub-tick error accumulates over a 2 s arc. +3. **EdgeSlide on near-vertical motion against a near-vertical + surface** — if the sphere even slightly grazes a wall while + ascending or descending, EdgeSlide projects motion tangent to the + wall, redirecting some Z velocity into XY. Less likely for + open-ground stationary jumps but could explain drift near + buildings. **Fix paths:** -a. **Pragmatic** (revert to legacy behavior): hard-snap - `rm.Body.Position = worldPos` on airborne UPs too. Diverges from - retail spec, but ACE behavior diverges from retail too (ACE - broadcasts mid-arc UPs while retail apparently doesn't). Masks - the drift identically to pre-M2. Lowest-risk visual fix. +a. **Skip initial-overlap depenetration when airborne** — gate the + "separate from initial contact plane" step inside + `ResolveWithTransition` on `isOnGround: true`. Trusts the previous + tick's resolve to have left the body in a non-overlapping position. + This is the most likely-correct fix if hypothesis (1) is right. -b. **Investigate** the actual XY source via VU/UP instrumentation - and `body.Velocity` snapshots, then fix the root cause. May be - an ACE-specific velocity-quirk we should clamp at - `OnLiveVectorUpdated`, or a clock-source mismatch in our Euler. +b. **Zero step-up/down for airborne sweeps** — pass + `stepUpHeight: 0f, stepDownHeight: 0f` when `rm.Airborne`. Kills + hypothesis (2) without other side effects (airborne bodies don't + step anyway). -c. **Hybrid**: keep airborne no-op for body.Position translation but - re-introduce a soft-correction on mid-arc UPs (server-position- - biased lerp) — slowly pulls body toward server truth without - the rubber-band. +c. **Stripped airborne sweep** — replace the full sphere sweep with + a simpler vertical sphere-vs-terrain intersection + wall-collision + stop. Loses some retail fidelity but eliminates all three + mechanisms. Probably overkill if (a) or (b) suffices. **Files:** -- `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveVectorUpdated` - L3228+ (sets velocity from wire verbatim) -- `src/AcDream.App/Rendering/GameWindow.cs` `OnLivePositionUpdated` - player-remote airborne no-op L3502+ (the no-op that exposed this) -- `src/AcDream.App/Rendering/GameWindow.cs` legacy airborne TickAnimations - L6478+ (gravity integration via UpdatePhysicsInternal) +- `src/AcDream.Core/Physics/PhysicsEngine.cs` — + `ResolveWithTransition` and any internal `CTransition` / + `find_valid_position` helpers. The initial-overlap depenetration + path is the primary investigation target. +- `src/AcDream.App/Rendering/GameWindow.cs:6478+` (legacy airborne + 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:** - 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. --- diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 2b07ebd..280e96b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3434,12 +3434,10 @@ public sealed class GameWindow : IDisposable // Adopt server's cell ID on every UP (airborne or grounded). // 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 - // the sphere sweep is skipped, K-fix15 landing detection never - // fires, and the body only re-grounds when the next UM forces - // ACE to broadcast a fresh IsGrounded=true UP that hits our - // landing transition branch below. + // the sphere sweep is skipped. Note: enabling the sweep also + // exposes a pre-existing depenetration bug — see #42. rmState.CellId = p.LandblockId; // Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous