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