acdream/docs/research/2026-07-07-remote-creature-deoverlap-handoff.md
Erik 7f7a78d3ea docs: remote-creature de-overlap — design spec + fresh-session handoff
The crowd-tightness residual from the #182 gate: monsters overlap (arms) in acdream but
barely in retail on the SAME ACE. Verified root (workflow wf_d2ff782f-9cb + source): retail
runs UpdateObjectInternal+transition on EVERY remote creature (CPhysics::UseTime 0x00509950,
no fork) so they de-overlap client-side, with the server pos a gentle MoveOrTeleport catch-up
target (0x00516330), NOT a hard-snap. acdream (a) hard-snaps NPC remotes to the raw overlapping
server pos (GameWindow.cs:5925) overwriting the swept de-penetration, and (b) forks player-remotes
(skip sweep) from NPCs (sweep at :10558 but driven by get_state_velocity, not the catch-up).

Collision math already exists + is faithful; the fix is the reconciliation (hard-snap→catch-up)
+ the movement model (synth-velocity→interp catch-up) — a delicate rework of the frozen R4/R5
remote-DR arc, staged NPC-first. Design spec + full handoff (verified code sites, retail anchors,
preserve-list, gotchas, slices) written for a fresh session. Implementation NOT started.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:29:06 +02:00

243 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: verbatim port of retail's per-remote physics tick so packed monsters de-overlap
**Date:** 2026-07-07 · **Status:** DESIGN SPEC WRITTEN + APPROVED, IMPLEMENTATION NOT STARTED
(fresh session). **Driving report (user, side-by-side vs retail on the SAME ACE):** monsters
packed around the player **overlap** (arms interpenetrating) in acdream; in the retail client they
barely overlap. That overlap is why there's "no room to slide out" of a crowd.
**Read these first (in order):**
1. Design spec: [`docs/superpowers/specs/2026-07-07-remote-creature-deoverlap-design.md`](../superpowers/specs/2026-07-07-remote-creature-deoverlap-design.md) — the full design.
2. This handoff — the executable detail + the exact verified code sites + what to preserve.
3. Physics digest banner (SSOT): `claude-memory/project_physics_collision_digest.md` (2026-07-07,
incl. the "the room is SLIDING, user RETRACTED the jiggle framing" note).
---
## 0. Session context — what shipped, what this is
This work is a **new, distinct thread** from the #182 *player* physics rebuild that shipped THIS
session (6 commits `8bb8b204``e3c4c59b`, Core 2617 / App 741 green, airborne-stuck **confirmed
fixed by the user's visual gate**: "Works better, don't get stuck in the animation"). That rebuild
is landed — do NOT redo it. Its own residual (the grounded ground-jam, #137 family) is a separate
Slice 3 tracked in ISSUES #182.
During that gate the user noticed the crowd still feels wrong and diagnosed it side-by-side vs
retail: **the monsters are packed tighter / overlapping in acdream**. That's THIS handoff.
---
## 1. Verified root cause (decomp + source — do NOT re-derive)
Research workflow `wf_d2ff782f-9cb` (4 read-only agents, Opus) + my own direct source reads. Retail
`pc:`/`0x…` anchors are into `docs/research/named-retail/acclient_2013_pseudo_c.txt`.
**Retail runs ONE unified per-object tick and collides every creature locally:**
- `CPhysics::UseTime` (**0x00509950**, pc:271640) walks **every** `CPhysicsObj` in the object table
and runs `update_object → UpdateObjectInternal` on each — **no fork** player-vs-remote. The player
only gets an extra camera callback.
- `UpdateObjectInternal` (**0x005156b0**) runs the full `transition` (collision) for any active
object with spheres whose origin moved. `CObjCell::find_obj_collisions` (**0x0052b750**, pc:308916)
iterates the cell's shadow list; creature-vs-creature collision is exempt **only** for a *viewer*
or *IGNORE_CREATURES* mover. So **moving remote creatures de-overlap against each other, client-side.**
- A remote stays "active" via the motion system feeding velocity each tick (`apply_interpreted_movement`
0x00528600 → `set_local_velocity` 0x005114d0 → `set_velocity` 0x005113f0 sets the 0x80 active bit).
**Retail's server position is a GENTLE dead-reckoning TARGET, not a hard-snap:**
- `SmartBox::HandleReceivedPosition` (**0x00453fd0**) → `CPhysicsObj::MoveOrTeleport` (**0x00516330**):
teleport-TS / no-cell → hard snap; has-velocity AND `player_distance < 96`**`InterpolateTo`**
(queue a DR node); has-velocity AND `≥ 96` → far snap.
- `InterpolationManager::adjust_offset` (**0x00555d30**) catches up toward the queued position at
**≤ 2× the creature's max animation speed × dt**. The per-tick **collision-resolved** position is
stored (`SetPositionInternal` writes `sphere_path.curr_pos`, NOT the raw target) and **persists**;
the next server update re-aims the catch-up **from wherever collision left the creature**. A
`node_fail_counter` blips to the server point only after ~5 genuinely-stuck frames.
**So retail's de-overlap is PROACTIVE (movement collision-stopped before interpenetration) and
PERSISTENT (never snapped-into-overlap).** ACE broadcasts each creature's own overlapping pathed
position; the CLIENT spreads them. (ACE/holtburger source are NOT checked out — empty submodule
dirs; the decomp + the #182 live cdb trace settle client-responsibility.)
**acdream's two divergences (both verified against source):**
1. **NPC (monster) `UpdatePosition` HARD-SNAPS** to the raw server position — `GameWindow.cs:5925-5926`:
`if (!snapSuppressedByStick) rmState.Body.Position = worldPos;` (suppressed only while a sticky
melee lease is armed). A packed monster teleports into the overlapping server position every UP,
**overwriting** the swept de-penetration. The sweep resolves *movement*, not a static overlap it
gets snapped into.
2. **A two-path fork retail doesn't have** (`GameWindow.cs:10076`):
- **Path A** — grounded *player* remotes (`IsPlayerGuid && !Airborne`, `:10076-10311`): advance
via `RemoteMotionCombiner.ComputeOffset` interp catch-up, **`ResolveWithTransition` NOT called**
(`:10274-10281` "collision is the sender's problem" — factually wrong per the decomp).
- **Path B** — NPCs/monsters + airborne player remotes (`:10312-10698`): **DO** call
`ResolveWithTransition` (`:10558`, `moverFlags: EdgeSlide`, self-skip `movingEntityId: kv.Key`)
— so a monster already collides against other creatures. But it drives the body from
`get_state_velocity()` (synth locomotion), and the NPC hard-snap (#1) overwrites the result.
**KEY INSIGHT: the collision math already exists and is faithful.** The bug is the **reconciliation
(hard-snap)** + the **movement model (synth-velocity instead of catch-up)** + the **fork**.
---
## 2. SCOPE REFINEMENT — read this before planning (discovered reading Path B)
The fix is **bigger than "replace the hard-snap."** Path B drives the body from
`get_state_velocity()` (the animation locomotion velocity), NOT the interp catch-up toward the
server target. For a monster to de-overlap, its per-tick **movement must chase the server target**
(so the sweep resolves *that* movement against neighbors and stops it at contact). So the faithful
fix **reworks Path B's movement model**: `synth-velocity → interp catch-up (ComputeOffset, like
Path A) → sweep → store resolved`. Retail's `UpdatePositionInternal` uses a **REPLACE dichotomy**
(the catch-up REPLACES the anim root motion when the queue is active; the anim just animates the
legs). This is a **delicate change to the FROZEN R4/R5 remote-DR arc** — treat it with care.
---
## 3. Exact code sites (all verified against source THIS session)
`RemoteMotion` type — `GameWindow.cs:441`: `.Body` (`:443` the sim PhysicsBody), `.Movement` (`:453`
R5 MovementManager; `.Motion => Movement.Minterp` `:455`, `.MoveTo` `:478`), `.Host` (`:485`
EntityPhysicsHost → TargetManager + the faithful `Motion.PositionManager` Sticky/Constraint facade),
`.Interp` (`:581` the `InterpolationManager` catch-up queue), `.Position` (`:590` the
`RemoteMotionCombiner`, NOT the retail facade).
**A. Remote `UpdatePosition` handler (`OnLivePositionUpdated`), `GameWindow.cs:~5640-5980`:**
- `:5655` `entity.SetPosition(worldPos)` (unconditional render snap, undone later per-branch).
- `:5665-5672` `ShadowObjects.UpdatePosition(entity.Id, worldPos, …)`**keep** (server-truth
shadow sync; retail `change_cell`/`AddShadowObject`). NOTE: this syncs the *collision broadphase*
target to server truth even though the rendered body de-overlaps — verify this doesn't re-pack.
- `:5724` `if (IsPlayerGuid(update.Guid))` → **the player-remote branch ALREADY implements retail
MoveOrTeleport** (airborne no-op `:5776`; landing `:5791`; far>96 snap+clear `:5827-5832`; near
`Interp.Enqueue(worldPos, heading, isMovingTo:false, currentBodyPosition:…)` `:5843-5847`). **This
is the model to extend to NPCs.**
- The `else` (NPC) branch, `:5879-5975`: computes `ServerVelocity` synth (`:5863`/`:5889`, keep for
`[VEL_DIAG]`); **`:5925-5926` the HARD-SNAP** `rmState.Body.Position = worldPos` (suppressed while
`snapSuppressedByStick`); `:5952` `rmState.CellId = p.LandblockId`; `:5967+` orientation hard-snap
(also stick-gated). **THIS is what Slice 1 replaces with the MoveOrTeleport Enqueue.**
**B. Path B tick (NPC/legacy), `GameWindow.cs:~10312-10698`:**
- `:10457-10460` `rm.Body.set_local_velocity(rm.Motion.get_state_velocity(), …)` when OnWalkable —
**the synth-velocity movement source to REPLACE with the interp catch-up.**
- `:10484-10492` manual omega/orientation integration (ObservedOmega) — **keep**.
- `:10512` `preIntegratePos = rm.Body.Position`.
- `:10521-10526` sticky delta via `rm.Host.PositionManager.AdjustOffset(pmDelta, dt)` +
`ApplyPositionManagerDelta`**keep, composes with the catch-up (retail adjust_offset order)**.
- `:10527-10528` `calc_acceleration()` + `UpdatePhysicsInternal(dt)` (integrates the synth velocity).
- `:10549-10583` **the SWEEP** `ResolveWithTransition(preIntegratePos, postIntegratePos, rm.CellId,
0.48f, 1.835f, 0.4f, 0.4f, isOnGround:!rm.Airborne, body:rm.Body, moverFlags:EdgeSlide,
movingEntityId:kv.Key)` — **KEEP; this is the creature-vs-creature de-overlap** (Slice 3 makes the
0.48/1.835 Setup-derived).
- `:10585` `rm.Body.Position = resolveResult.Position` + `:10586-10587` CellId writeback.
- `:10606+` the #173 airborne reflect (old AD-25 airborne-only bounce) — leave for now (or later
fold into `PhysicsObjUpdate.HandleAllCollisions` like the player path; NOT Slice 1).
**C. Path A tick (player remote, ComputeOffset, NO sweep), `GameWindow.cs:10076-10311`:**
- `:10173` `rm.Position.ComputeOffset(dt, rm.Body.Position, seqVel, ori, rm.Interp, maxSpeed,
terrainNormal)` — the catch-up model (REPLACE dichotomy). `:10274-10281` the "ResolveWithTransition
NOT called" opt-out. **Slice 2 adds the sweep here and unifies with Path B.**
**D. `RemoteMotionCombiner.ComputeOffset`, `RemoteMotionCombiner.cs:59-115`:** interp catch-up
(`interp.AdjustOffset` toward the queue head) REPLACES the frame when non-zero; else seqVel anim
fallback projected on the terrain plane. This is the movement primitive both paths should use.
**E. Substrate (KEEP, already retail-shaped):** shadow registry `RegisterLiveEntityCollision`
`GameWindow.cs:4171` (`:4276 RegisterMultiPart`, `IsCreature` `:4271`); `CollisionExemption`
`CollisionExemption.cs:59-129` (creature-vs-creature exempt only for viewer/ignore-creatures mover —
an NPC mover with `EdgeSlide` is NOT exempt, so it DOES collide with other creatures).
---
## 4. The plan (slices) — see §2.2/§2.5 of the design spec
**Slice 1 — NPC de-overlap (the reported symptom). The two changes are COUPLED, land together:**
1. **NPC UP handler** (`:5925-5926` + orientation `:5967+`): replace the hard-snap with the retail
`MoveOrTeleport` routing — mirror the player-remote branch (`:5822-5848`): far>96 → snap + clear
queue; near → `Interp.Enqueue`; teleport/parent → snap. **Keep** the `snapSuppressedByStick`
escape (it's retail's "armed stick owns the frame, server correction can't fight it", TS-41/44).
Keep the orientation hard-snap (retail hard-snaps orientation on UP).
2. **Path B tick** (`:10457-10528`): replace the `get_state_velocity` movement with the interp
catch-up as the movement source (use `rm.Position.ComputeOffset` / `rm.Interp.AdjustOffset` toward
the queued server target, REPLACE dichotomy), compose the sticky delta, **KEEP the sweep** (`:10558`)
and store `resolveResult.Position`. The anim (legs) still plays via the sequencer. `ServerVelocity`
stays for diagnostics only.
- **Gate:** monsters de-overlap side-by-side vs retail; no remote jitter/rubber-band/desync; sticky
melee (#171) unbroken; walk/run/jump/land unchanged; Core 2617 / App 741 green.
**Slice 2 — unify the fork:** collapse Path A into the same catch-up+sweep model (player remotes gain
the sweep → packed *players* de-overlap too). Per Code Structure Rule 1 (no new feature body in the
>10k-line `GameWindow`), extract a **`RemotePhysicsUpdater`** class (`src/AcDream.App/Physics/` or
`Rendering/`) owning the unified per-remote `UpdateObjectInternal` chain (interp delta → sweep →
commit); `GameWindow`'s loop shrinks to `RemotePhysicsUpdater.Tick(rm, dt)`. Extract the
`MoveOrTeleport` routing into a shared method so NPC + player-remote reconciliation is ONE impl.
Retire the "remotes skip the transition" adaptation (`:10089`/`:10275`, filed #40, ISSUES.md ~:4899).
**Slice 3 — Setup-derived mover sphere:** replace the hard-coded `0.48f/1.835f` (`:10551-10556`) with
the creature's Setup sphere / `ObjScale` so differently-sized creatures de-overlap at true radii.
---
## 5. What to PRESERVE (regression watch — the R4/R5 arc has many hard-won fixes)
- The **sweep + transition internals** (`ResolveWithTransition` and below), **shadow registry**,
**`CollisionExemption`** — untouched (already faithful).
- The **R5 managers** (`MovementManager`/`MoveToManager`/`TargetManager` + `Motion.PositionManager`
Sticky/Constraint) — TICKED by the unified update, not rewritten.
- **Sticky melee #171** (`snapSuppressedByStick` escape + the sticky delta at `:10521-10526`) — MUST
survive; it's the "monster facing while attacking" fix.
- The **airborne remote path** (`!IsGrounded` no-op `:5776`, landing transition `:5791`, gravity
integration between UPs) — MUST survive.
- The **omega/orientation manual integration** (`:10484-10492`) and orientation hard-snap — keep.
- The **`node_fail_counter` blip watchdog** in `InterpolationManager` — keep (retail's stuck→snap
escape; without it a genuinely-blocked remote freezes).
- The **#182 local-player rebuild** (this session) — orthogonal, untouched.
## 6. Gotchas
- **Don't double-move:** the catch-up REPLACES the synth-velocity (retail's REPLACE dichotomy), it is
NOT additive on top of `get_state_velocity`.
- **The two Slice-1 changes are coupled** — Enqueue is useless unless Path B consumes the queue; a
half-change (Enqueue without the catch-up movement) breaks DR (body stops tracking the server).
- **`ShadowObjects.UpdatePosition` (`:5665`) still syncs the broadphase to server truth** — verify
the de-overlapped *rendered* body and the server-truth *collision target* don't fight (retail's
shadow follows the collided position; check whether acdream should sync the shadow to the
collision-resolved position instead of raw server truth). Potential subtle bug — probe it.
- **The sweep only de-overlaps MOVING creatures** (it resolves movement, not static overlap). Retail
never lets deep static overlap form (proactive). The converging-pair test must drive real movement.
## 7. Validation
- **Conformance test** (`RemotePhysicsUpdaterTests`, easiest after the Slice-2 extraction): two
registered creature shadows given converging catch-up targets → assert they settle at
contact-distance (sum of radii), not overlapping; a `MoveOrTeleport` routing test (near→enqueue,
far→snap, teleport→snap); a persistence test (a de-penetrated position survives the next server UP).
- **Suites green:** Core 2617 / App 741 (no regression to remote anim, DR smoothness, sticky #171,
the #182 player fix).
- **Visual gate (acceptance):** side-by-side vs the retail client on the SAME ACE — packed monsters
spread to retail spacing (arms no longer interpenetrating); remotes don't jitter/rubber-band/desync;
sticky-melee facing (#171) unbroken; remote walk/run/jump/land unchanged.
- **Apparatus:** `ACDREAM_REMOTE_VEL_DIAG=1` (existing, UP/seq pace); add a `[remote-deoverlap]` probe
(rendered pos vs raw server UP + neighbor overlap depth) if a residual needs it.
## 8. Risk
Touches the **frozen R4/R5 remote-DR arc**. The synth-velocity model was tuned against live ACE pace
over the R4/R5 sessions; replacing it with the catch-up model risks regressing remote walk/run
smoothness, the slope "staircase" fix, and the sticky melee. Mitigate: stage NPC-first + gate before
Slice 2; preserve every escape (§5); keep the sweep/registry/managers; the `node_fail_counter`
prevents a stuck-remote freeze.
## 9. Register bookkeeping (do in the landing commits)
- Retire the "remotes skip the transition / server already collision-resolved" adaptation
(`GameWindow.cs:10089`/`:10275`, #40) — its "Risk if assumption breaks" IS the arms-overlap. Retire
in the Slice-2 commit that adds the sweep to Path A.
- Add rows for: the NPC `MoveOrTeleport` near-distance constant (96 m), the sticky-suppression
retention, and (if kept) the shadow-follows-server-truth vs collision-resolved decision.
## 10. Retail anchors (addresses, for `grep named-first` in the fresh session)
`CPhysics::UseTime` 0x00509950 · `update_object` 0x00515d10 · `UpdateObjectInternal` 0x005156b0 ·
`UpdatePositionInternal` 0x00512c30 · `transition` 0x00512dc0 · `find_obj_collisions` 0x0052b750 ·
`MoveOrTeleport` 0x00516330 · `HandleReceivedPosition` 0x00453fd0 · `InterpolateTo` 0x005104f0 /
`InterpolationManager::InterpolateTo` 0x00555b20 · `adjust_offset` 0x00555d30 · `UseTime` 0x00555f20 ·
`set_local_velocity` 0x005114d0 · `apply_interpreted_movement` 0x00528600.
## 11. Pointers
- Design spec: `docs/superpowers/specs/2026-07-07-remote-creature-deoverlap-design.md`.
- Physics digest (SSOT + DO-NOT-RETRY): `claude-memory/project_physics_collision_digest.md`.
- The #182 player rebuild (shipped this session, orthogonal base): plan
`docs/superpowers/plans/2026-07-07-player-physics-update-verbatim-rebuild.md`; the histogram
`tools/analyze_resolve_capture.py`.
- Research: workflow `wf_d2ff782f-9cb` (4-agent read-only sweep; findings distilled into §1-§3 here).