acdream/docs/research/2026-05-05-issue-42-handoff.md
Erik ce638eb56f docs(research): expand #42 handoff prompt for fresh-session pickup
Replaces the original 96-line note with a detailed self-contained
brief targeted at someone picking up #42 cold in a new session.

Adds:

- Explicit ruled-out list (wire data, Euler error, stale velocity,
  diagnostic instrumentation) — saves rediscovering dead ends.
- The user's "buildings + jumping puzzles" constraint that rules out
  blanket sweep-disable.
- Specific file/line targets in PhysicsEngine.cs (470, 478-491,
  493-519, 521-530, 532, 534-558) and TransitionTypes.cs (786-846,
  1305-1311) with a brief reading order.
- Phase 1 / Phase 2 / Phase 3 investigation plan with concrete
  diagnostic harness (`ACDREAM_AIRBORNE_DIAG=1` + `[SWEEP]` log) and
  direction-correlation test.
- Per-hypothesis fix paths so the agent doesn't re-derive them from
  the diagnosis.
- Full acceptance criteria including build/test gates and visual
  test sequence (flat / hillside / running / doorway / puzzle / land).
- Hard rules (don't blame ACE, don't disable sweep, don't touch L.3
  motion code, don't reduce sphere dims, etc.).
- cdb breakpoint recipe for retail-vs-acdream A/B comparison.
- Pre-session reading list with line numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:02:58 +02:00

391 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff prompt — fix #42 PhysicsEngine airborne XY drift
Paste this into a fresh Claude Code session at the acdream repo root.
The branch you'll work on is `main` at or after commit `086e65d`
("Merge L.3 motion port"). Read `CLAUDE.md` first for project conventions.
## What you're fixing
When a retail-controlled toon jumps **in place** (no horizontal input,
just press jump) while observed through acdream, the visible jump arc
renders with a **~1 m horizontal offset** from the actor's actual XY
position. Body lands at the offset. Position snaps back to the actor's
real XY when the actor next sends any UM (turn / step / etc.).
User's exact wording: *"I stand at position X and jump, it looks like
im jumping slightly to the left of X like 1 m-ish (if I observe
jumping char from behind). It also lands at X + 1 m-ish. Position
resets to X when I issue some other command to the client like turning."*
This is **filed as `#42` in `docs/ISSUES.md`** with severity MEDIUM.
Pre-existing PhysicsEngine bug, exposed by L.3 M2 (which removed the
mid-arc UP hard-snap that was masking it) and by L.3 M4 (which
re-enabled `ResolveWithTransition` per-tick airborne).
## What's been ruled out (already, A/B-tested 2026-05-05)
The drift is **definitively inside `PhysicsEngine.ResolveWithTransition`**.
Verified by toggling a single line in `GameWindow.OnLivePositionUpdated`:
- `rmState.CellId = p.LandblockId;` REMOVED → sweep skipped → jumps
render geometrically clean (no XY drift), but body falls through
the floor (no terrain catch).
- `rmState.CellId = p.LandblockId;` PRESENT → sweep runs → jumps
land cleanly on the floor, but arc shows ~1 m horizontal drift.
That A/B narrows the bug to one place. **Do not waste time** on:
- ACE wire `VectorUpdate` velocity components (not the source — drift
exists with sweep enabled regardless of wire data)
- Local Euler integration error (not the source — drift is geometric,
visible immediately on jump start)
- Stale `body.Velocity` from before the jump (M3 grounded path
zeroes it every tick; verified clean)
- Wire diagnostic instrumentation (`[VU.WIRE]`, `[UM_RAW]`, etc.) —
irrelevant. The bug is in the sweep, not the inputs.
The bug is **inside `ResolveWithTransition`** (or one of the helpers
it delegates to: `Transition`, `SpherePath`, `CollisionInfo`,
`AdjustOffset`).
## Why the fix can't just disable the sweep
The user explicitly raised: *"What about jumping in to buildings or
on a jumping puzzle?"*
Right. We need full sphere-vs-geometry resolution mid-arc for:
- Building doorways (jumping into a doorway you can stand in)
- Jumping puzzles (threading between platforms, around obstacles)
- Ceiling clips (hitting an overhang at jump apex)
- Edge collision (clipping against the corner of a platform)
So the fix has to **keep the sweep running, but stop it from pushing
horizontally on stationary jumps**. Pure-vertical motion against a
roughly-horizontal floor must produce zero XY delta from the sweep.
## The three ranked hypotheses
These are the in-sweep mechanisms most likely producing the drift.
Listed by probability; (1) is the leading candidate.
### H1: Initial-overlap depenetration along non-+Z terrain normal
At jump start the collision sphere is touching the floor: `body.Z` is
at ground level, sphere radius extends downward into the terrain
triangle. The sweep's first tick has to resolve that overlap by
**separating the sphere along the contact normal**.
Outdoor terrain is rarely perfectly flat. Most terrain triangles tilt
0.510° from horizontal. Their normals have a small horizontal
component. When the sweep separates the sphere along that tilted
normal, the separation has horizontal magnitude → body shoves
sideways → drift carries through the rest of the arc.
**Direction-correlation test confirms this hypothesis if true:**
jumping at multiple landblock positions, the drift direction should
correlate with **local terrain slope orientation**, not the actor's
facing. (If drift is always relative to actor's facing, H1 is wrong
and H2 or H3 is the source.)
### H2: Step-down probe firing despite `isOnGround: false`
`PhysicsEngine.ResolveWithTransition` is called with `isOnGround:
!rm.Airborne``false` for airborne. But the sweep's internals may
still execute step-down logic regardless of that flag. Step-down
searches **horizontally** within `stepDownHeight` (0.4 m) for the
nearest walkable surface — even a small horizontal probe can shift
an airborne body sideways if the search finds a "better" surface.
Already a known asymmetry exists in this region — see `K-fix7` /
`K-fix9` comments in `GameWindow.cs:6611-6620` and the matching
contact-plane gating in `PhysicsEngine.cs:493-519`. Step-up/down may
have a similar gap.
### H3: EdgeSlide on near-vertical motion grazing a near-vertical surface
If the sphere even slightly grazes a wall while ascending or
descending, `EdgeSlide` projects the motion vector tangent to the
wall surface, redirecting some Z velocity into XY. Less likely for
open-ground stationary jumps but cannot be ruled out without testing
in different environments.
## Files to investigate
```
src/AcDream.Core/Physics/PhysicsEngine.cs (658 LOC)
Line 470: ResolveWithTransition entry point
Lines 478-491: Transition / ObjectInfo setup
Lines 493-519: K-fix7 contact-plane gating (existing airborne fix)
Lines 521-530: SlidingNormal seed
Line 532: SpherePath.InitPath (path setup)
Lines 534-558: WalkablePolygon seed (grounded only)
src/AcDream.Core/Physics/TransitionTypes.cs (2200 LOC)
Top-level: Transition, SpherePath, CollisionInfo, ObjectInfo classes
Search for: find_valid_position, step_sphere, AdjustOffset,
StepUp / StepDown / EdgeSlide branches
Lines 786-846: existing step-down + edge-slide branch logic
(where H2/H3 might be firing inappropriately for
airborne)
Lines 1305-1311: another step-down/contact branch worth checking
src/AcDream.Core/Physics/CollisionPrimitives.cs (718 LOC)
Sphere-vs-triangle math; depenetration-related primitives if any
```
The L.4 collision-resolution work added the existing airborne gates
(K-fix7 ContactPlane, K-fix9 ContactPlane during VectorUpdate) but
didn't touch step-up/down or initial-overlap depenetration paths.
Those are the gaps.
## Reference: retail named-retail decomp
Mandatory cross-reference. Located at:
```
docs/research/named-retail/acclient_2013_pseudo_c.txt (1.4 M lines, PDB-named)
docs/research/named-retail/acclient.h (verbatim retail headers)
docs/research/named-retail/symbols.json (greppable name → address)
```
Functions to grep for in the named pseudo-C:
- `CTransition::init` @ `0x00509dd0` — explicitly clears
`contact_plane_valid = 0` at start of every resolve. Confirms the
airborne should not be running on a stale contact plane (we already
match this — K-fix7).
- `CTransition::find_valid_position` — entry to the actual sweep.
Read this carefully for the airborne-vs-grounded branching.
- `CSpherePath::InitPath` — path-setup; check whether retail does
any initial-overlap separation here.
- `CSpherePath::step_sphere` — the per-step inner loop.
- `CTransition::ValidateWalkable` — rebuilds contact plane when sphere
bottom is within EPSILON of terrain plane (per K-fix7 comment); for
airborne this should not establish a plane.
- `CCollisionInfo::set_contact_plane` and the depenetration paths
that use it.
**Compare retail behavior against our port byte-for-byte for the
specific airborne-stationary-jump path.** Our port may have
inadvertently introduced behavior retail doesn't have (e.g.,
unconditional initial separation), or omitted a retail gate (e.g.,
"skip depenetration if `transient_state.Contact == 0`").
## Investigation plan
Two phases. Don't skip phase 1.
### Phase 1: Confirm hypothesis (≤ 2 hours)
1. Verify the M4 `CellId` fix is on `main` by grepping `GameWindow.cs`
for `rmState.CellId = p.LandblockId`. It should be present at the
top of the M2 player-remote branch in `OnLivePositionUpdated`.
2. Add lightweight diagnostic logging inside `ResolveWithTransition`:
```
[SWEEP] guid=<guid> pre=(x,y,z) target=(x,y,z) post=(x,y,z) airborne=<bool>
```
Gate behind `ACDREAM_AIRBORNE_DIAG=1` so the diagnostic doesn't
spam logs in normal use.
3. Have the user jump in place at 3+ different landblock positions
with visibly different terrain orientation:
- flat plaza tile
- hillside facing east
- hillside facing north
For each, capture the sweep's `pre→target→post` deltas across the
first 5 frames of the arc.
4. **Check direction correlation:**
- If drift direction varies with terrain orientation → H1 confirmed.
The first frame of the sweep is shifting the body horizontally
by an amount proportional to the local terrain slope.
- If drift direction is always relative to actor facing → H1 wrong;
check H2 (step-down probe) by comparing sweeps with non-zero vs
zero `stepDownHeight`.
- If no drift in open ground but drift near walls → H3.
5. Stop at the first confirmed hypothesis. Move to phase 2 with
precise knowledge of which mechanism is firing.
### Phase 2: Fix (14 hours depending on hypothesis)
**If H1 confirmed (most likely):**
- Find the depenetration / "separate sphere from initial contact"
path in `Transition` / `SpherePath` / `CollisionInfo`.
- Gate it on `ObjectInfo.State.HasFlag(ObjectInfoState.Contact)`
(i.e., grounded). Trust the previous tick's resolve to have left
the body in a non-overlapping position.
- Compare against retail's `CTransition::init` and `find_valid_position`
— likely retail has this gate and we don't.
**If H2 confirmed:**
- Find the step-up/step-down branches in `TransitionTypes.cs`.
- Gate them on `oi.Contact` (already exists at line 787 — verify it
fires correctly when airborne).
- May need to additionally zero `StepUpHeight` / `StepDownHeight`
when airborne at the call site in `GameWindow.cs`.
**If H3 confirmed:**
- Find the EdgeSlide branch in `TransitionTypes.cs`.
- Add a gate that EdgeSlide only fires when motion has horizontal
velocity component above a threshold (so pure-vertical motion is
exempt).
- Verify against retail's edge-slide branch behavior.
### Phase 3: Visual verification
User will run retail + acdream side-by-side. Test cases:
- ✅ Jump in place on flat ground — no XY drift
- ✅ Jump in place on hillside (different orientations) — no XY drift
- ✅ Jump while running forward — arc carries forward momentum (XY
delta from initial wire velocity, NOT from sweep)
- ✅ Jump into a building doorway — body collides with door frame /
threads through doorway correctly
- ✅ Jump from a platform onto another (jumping puzzle mechanic) —
body lands on the target platform
- ✅ Land on slope — sequencer leaves Falling correctly
If any of these regress, the fix is wrong or incomplete. Iterate.
## Diagnostic toolchain
### cdb attach to retail (high-value when comparing retail vs ours)
Toolchain documented in CLAUDE.md "Retail debugger toolchain" section.
The relevant breakpoints for #42:
```
bp acclient!CTransition::init "r $t0 = @$t0 + 1; gc"
bp acclient!CTransition::find_valid_position "r $t1 = @$t1 + 1; gc"
bp acclient!CSpherePath::step_sphere "r $t2 = @$t2 + 1; gc"
```
Have the user (running retail with cdb attached) jump in place. The
hit counts and any printed state from breakpoint actions reveal
retail's actual airborne sweep call pattern. Compare against
acdream's instrumented `[SWEEP]` log. Differences are bugs.
**Important warnings from CLAUDE.md:**
- `qd` / `q` / `qq` are FORBIDDEN inside breakpoint actions — use
`.detach`.
- High-frequency breakpoints lag retail to ACE timeout — counter-only
actions for hot paths.
- `cdb -pd` does NOT survive `Stop-Process -Force`; detach cleanly.
### Live launch
```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_AIRBORNE_DIAG = "1" # enable sweep diagnostic once added
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug *>&1 |
Tee-Object -FilePath "launch-42.log"
```
Wait 35 s between launches (ACE session cleanup race; CLAUDE.md).
### Test character
`+Acdream` at server guid `0x5000000A`. Per CLAUDE.md, this is the
GM-marker test toon. The user will control it (or another retail toon)
from a parallel retail client and you observe in acdream.
## Acceptance criteria
The fix is done when ALL of these hold:
- [ ] Stationary jump on flat ground: arc + landing render at actor's
actual XY (zero perceptible drift, zero snap-back on next UM).
- [ ] Stationary jump on hillside: same — drift independent of
terrain orientation.
- [ ] Forward-running jump: arc carries actor's forward XY velocity
(this is server-driven, expected to track).
- [ ] Jump into a doorway: collision resolves correctly.
- [ ] Jump puzzle: thread between two platforms.
- [ ] Build green (`dotnet build`).
- [ ] Tests green (`dotnet test`) — modulo the 8 pre-existing
`Core.Tests` failures unrelated to this work.
- [ ] `[SWEEP]` diagnostic shows zero XY change on a stationary jump's
first sweep frame (the H1 confirmation residue).
## Hard rules
- **Don't blame ACE.** ACE is fixed; retail clients work against ACE
without this drift, so our PhysicsEngine port is the bug.
- **Don't disable the sweep wholesale.** The user explicitly needs
jumping puzzles + wall collision to keep working.
- **Don't change `OnLivePositionUpdated` or `TickAnimations`.** The
L.3 motion-port work is correct; this is a `PhysicsEngine` bug.
- **Don't touch `MotionInterpreter` or `AnimationSequencer`.** Same
reason.
- **Don't reduce sphere dims** to "avoid intersection". 0.48 m radius
/ 1.2 m height is retail human-scale and matches local-player
collision; changing it on remotes only would cause asymmetric
behavior between local jumps (working) and remote jumps (different
collision profile).
- **Don't add diagnostic logging that prints every frame** without
an env-var gate — already burned the `[VEL_DIAG]` budget once.
- **Don't commit a partial fix that breaks any test in the
acceptance criteria.** Trade off completeness for correctness.
## Pre-session reading list
In order, before writing any code:
1. `docs/ISSUES.md` § `#42 — Airborne XY drift` (the spec)
2. This handoff prompt (you're here)
3. `src/AcDream.Core/Physics/PhysicsEngine.cs:470-560`
(`ResolveWithTransition`)
4. `src/AcDream.Core/Physics/TransitionTypes.cs:780-870`
(existing step-down + edge-slide branches)
5. Grep `K-fix7` and `K-fix9` in `GameWindow.cs` and
`PhysicsEngine.cs` (these are the prior airborne-related fixes;
matching style is helpful)
6. The L.3 motion-port research at
`docs/research/2026-05-04-l3-port/` —
especially `01-per-tick.md` § 5 (`CPhysicsObj::transition`) and
`01-per-tick.md` § 6 (`SetPositionInternal`) for retail's
per-tick collision-sweep contract.
7. Retail's `CTransition::init` at `acclient_2013_pseudo_c.txt` line
271954 (cited in K-fix7 comment).
## Operating notes
- **Branch off `main`.** Do not work directly on main; use a feature
branch for the fix. Worktree is fine; CLAUDE.md and prior sessions
use `.claude/worktrees/`.
- **TDD where it fits.** A unit test that asserts
`ResolveWithTransition` returns zero XY delta for a stationary
vertical sweep on a tilted-normal triangle would lock the fix.
Existing `tests/AcDream.Core.Tests/Physics/PhysicsEngineTests.cs`
is the right home.
- **Visual verification is the acceptance test.** The unit test
proves the math but the user's eye proves the bug. Plan time for
the user to run the test sequence above.
- **One-session estimate: half a day to a day.** Phase 1 should
identify the hypothesis in under 2 hours; Phase 2 fix depends on
which one. Plan for two iterations if the first fix attempt
doesn't fully resolve.
## Out of scope
- `#41` (sub-decimeter velocity-synthesis blips) — separate issue.
- Refactoring NPC vs player-remote convergence in `TickAnimations`
(filed in audit § 6 of `06-acdream-audit.md`) — separate session.
- Local-player jump behavior — already works; don't touch.
- ACE-side fixes — out of scope.
## Final advice
The L.3 motion-port history (commits `de129bc`..`5cc2812`) shows what
this codebase rewards: spec-faithful reads of named-retail before
writing any code. Apply the same here. The fix is probably small —
maybe 520 lines — but finding it requires reading retail's
`find_valid_position` and our `Transition` carefully and identifying
the diff. Don't guess; verify.