The #182 CSphere port (96ae2740) failed its visual gate and introduced an
airborne "stuck in the falling animation" regression. A player-attributed retail
cdb trace (tools/cdb/retail-crowd-jump3.cdb) proved retail's LOCAL client fully
runs player-vs-creature collision (76 land_on_sphere, 188 COLLIDED, 130 SLID,
~78% OK, glides across) -- NOT server-authoritative (an earlier unfiltered
land_on_sphere=0 read was a false lead the attributed trace refuted).
acdream's same-repro capture: 50.9% OK, 22.4% stuck, 115 airborne-stuck. Root
divergence: retail CPhysicsObj::UpdateObjectInternal (0x005156b0, pc:283688) sets
cached_velocity = (resolved - old)/dt -- velocity from ACTUAL movement, so a
blocked jump collapses to ~0 -> gravity -> the player falls/glides. acdream
integrates velocity + reflects on collision (PlayerMovementController ~:1008-1069),
so the jump velocity (~18) persists against the creature -> hang.
Fix = verbatim rebuild of the per-frame player-physics loop (UpdateObjectInternal
chain), velocity model first, transition internals kept. Full design +
retail function inventory + the capture apparatus + retail target numbers:
docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md.
Implementation deferred to a fresh session (user decision). Also files #183
(floating distant scenery, observed during testing). #182 stays as the base.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
Design: verbatim rebuild of the per-frame player physics update loop
Date: 2026-07-07 · Status: DESIGN APPROVED (implementation deferred to a fresh session) Driving issue: #182 (player-vs-monster crowd collision) + the airborne "stuck in the falling animation" regression it exposed. Brainstorm decisions (user): verbatim rebuild (highest fidelity); scope = the per-frame update LOOP, keep everything already faithful; stage the velocity model first and measure; do the implementation in a NEW session.
1. Problem (from the user, live)
- Crowd jam — packed in a group of monsters, the player gets stuck and can't push through / wiggle out. Retail lets you shove monsters aside and keep moving.
- Airborne stuck — jumping up into a crowd, retail lets you land/glide across the monster tops; acdream hangs in the falling animation unable to rise or fall.
The #182 CSphere-family port (committed 96ae2740) was faithful but aimed a layer
too low: it fixed the per-contact sphere response, did NOT fix the crowd feel, and
introduced the airborne-stuck regression (before #182 the hand-rolled
de-penetration shoved the player up onto monsters).
2. Evidence — the traces (the load-bearing part; do not re-derive)
Retail, player-attributed cdb trace (tools/cdb/retail-crowd-jump3.cdb)
Filtered to the local player's CPhysicsObj (captured from set_local_velocity),
during a jump-into-a-crowd repro:
pValidate=1482 pCOLLIDED=188 (12.7%) pSLID=130 (8.8%) pLand=76 → ~78% OK
- The retail local client fully runs player-vs-creature collision — 76
CSphere::land_on_sphere(airborne creature landings), 188 COLLIDED, 130 SLID — and still nets ~78% OK. NOT server-authoritative. (An earlier v2 unfiltered read ofland_on_sphere=0was a false lead — the attributed trace refuted it.) - The player glides across / lands on the crowd via local physics.
acdream, ACDREAM_CAPTURE_RESOLVE JSONL capture (same repro, current #182 build)
Of the player's move-intent resolves (2883 of 41832 total):
| acdream | retail | |
|---|---|---|
| OK (reached target) | 50.9% | ~78% |
| partial / slid | 26.7% | ~9% |
| stuck (reverted, no advance) | 22.4% | ~13% |
| airborne-stuck | 115 frames | (glides off) |
acdream jams ~2× harder, and the 115 airborne-stuck frames ARE the falling-animation
bug. In those frames: velocity (0,0,~18) (a jump), a near-horizontal creature
collision normal (e.g. (-0.96,-0.25,-0.15)), the transition reverts (no advance),
and the sliding normal flips frame-to-frame — the #137 anti-parallel-absorb wedge.
3. Root divergence (verified against decomp + capture)
Retail CPhysicsObj::UpdateObjectInternal (0x005156b0, pc:283611) recomputes the
player's velocity from the ACTUAL collided movement every frame:
newFrame = UpdatePositionInternal(this, dt) // integrate velocity + gravity
transition = CPhysicsObj::transition(this, m_position, newFrame, 0) // collision sweep
if transition == null: set_frame(newFrame); cached_velocity = 0
else:
offset = get_offset(m_position, transition.sphere_path.curr_pos)
cached_velocity = offset / dt // ← VELOCITY = ACTUAL MOVEMENT / dt (0x005158cb-005158ff)
SetPositionInternal(transition)
So when the player is blocked (resolved ≈ old position), cached_velocity → ~0,
gravity resumes next frame, and the player falls / settles / glides off the
obstacle. The collision naturally bleeds the velocity — there is no reflection step.
acdream uses a different model: PhysicsBody integrates velocity/gravity, then
PlayerMovementController (~:1008-1069) reflects the into-wall component on
collision (Velocity = v + n·k) and clamps Z on landing. For a straight-up jump into
a creature whose collision normal is horizontal, the reflection barely touches +Z, so
the jump velocity persists — the player keeps shoving up into the creature and
hangs. This is the airborne-stuck, and the same "velocity doesn't bleed on block"
shape drives the general 2× jam.
This velocity model is the centerpiece of the fix.
4. Design — verbatim rebuild of the per-frame loop
Port retail's UpdateObjectInternal chain verbatim into a single owner of the
per-frame player physics. Reuse the already-faithful transition internals as the
collision primitive — do not touch them.
4.1 Retail functions to port (chain of UpdateObjectInternal)
| Function | Address | Role |
|---|---|---|
CPhysicsObj::UpdateObjectInternal |
0x005156b0 | per-frame entry: integrate → transition → commit → velocity |
CPhysicsObj::UpdatePositionInternal |
(grep) | integrate velocity + gravity + forces → candidate frame |
CPhysicsObj::calc_acceleration |
0x00510950 | gravity / friction / buoyancy |
CPhysicsObj::handle_all_collisions |
0x00514780 | retail's collision velocity response (verify role vs the cached_velocity model) |
CPhysicsObj::SetPositionInternal |
0x00515330 / 0x00515bd0 / 0x00516040 (3 overloads) | commit the transition's resolved position |
CPhysicsObj::transition |
(grep) | collision sweep — maps to acdream ResolveWithTransition, reused as-is |
4.2 In scope (rebuilt verbatim)
The per-frame player physics loop: velocity + gravity integration
(UpdatePositionInternal/calc_acceleration), the transition orchestration
(call → commit), the velocity-from-movement recompute (cached_velocity = (resolved − old)/dt), and handle_all_collisions.
4.3 OUT of scope (kept — already faithful, per the user "keep everything faithful")
The transition INTERNALS: ResolveWithTransition and everything under it — BSPQuery,
the CSphere/CylSphere collision families (incl. the #182 port), cell membership,
terrain, streaming. Retail's transition calls these primitives and so does ours.
4.4 Architecture
A new PhysicsObjUpdater (or a PhysicsBody.UpdateObjectInternal(dt) method) owns
the retail chain verbatim. It absorbs: PhysicsBody's ad-hoc gravity/friction
integration AND PlayerMovementController's post-resolve bounce/reflect/clamp block
(~:1008-1069). PlayerMovementController shrinks to: input → velocity/jump intent →
hand off. The retail velocity-from-movement model replaces reflect-and-clamp
wholesale. Register a divergence-register row retiring the reflect model + any
adaptation rows the port introduces.
4.5 Staging (velocity model first, measured)
- Slice 1: port
UpdatePositionInternal+calc_acceleration+ thecached_velocity = (resolved−old)/dtrecompute, replacing the integrate+reflect path. A/B against the crowd capture: require the airborne-stuck count → 0 and OK% to climb toward retail's ~78%. Gate (suites + a visual pass on ordinary jump/walk/fall) before continuing. - Slice 2:
handle_all_collisionsverbatim (once Slice 1 shows what residual the velocity model leaves) + any remainingUpdateObjectInternalorchestration. - Slice 3+: close the residual ground-jam gap if the velocity model alone doesn't reach ~78% OK; re-measure each slice against the capture.
5. Validation
- Apparatus (built this session, reuse):
ACDREAM_CAPTURE_RESOLVE=<path>JSONL of every player resolve; thepyhistogram (OK / partial / stuck / airborne-stuck; see §2). Retail target: ~78% OK, 12.7% COLLIDED, 8.8% SLID, 0 airborne-stuck. Retail cdb scripts:tools/cdb/retail-crowd-jump{2,3}.cdb(v3 = player-attributed; safe auto-detach viavalidate_transitionterminal + top-levelqd). - Conformance tests: velocity-from-movement (blocked → velocity → ~0); jump +
gravity integration; the
UpdateObjectInternalsequence; a crowd replay. - Suites stay green: Core 2603 / App 741 (no regression to walking/stairs/slopes/ walls/streaming — the transition internals are untouched).
- Visual gates: (a) crowd glide/land like retail (the #182 symptom); (b) a regression pass on normal locomotion — flat walking, slopes, stairs, jumping, falling, wall-slide.
6. Risk
Highest-risk option: it replaces the core of every jump/fall/step. Mitigation: the scope boundary (transition internals untouched), the measured slicing (Slice 1 behind an A/B compare), and heavy gating. If Slice 1 regresses ordinary locomotion, bisect within the velocity model before proceeding.
7. Open items for the new session (verify during writing-plans)
- Read
UpdatePositionInternal,calc_acceleration(0x00510950), andhandle_all_collisions(0x00514780) fully — confirm the integration + the role ofhandle_all_collisionsvs the cached_velocity model (does retail ALSO reflect, or is velocity purely movement-derived?). - Confirm which
SetPositionInternaloverload the player path uses (3 exist). - Decide whether the general ground-jam (22% stuck vs retail 13%) is fully explained by the velocity model or needs a second divergence (de-penetration / sliding-normal provenance — the #137/TS-45 wedge family). Measure after Slice 1.
- #182 stays as the base (user decision). NOTE: until this rebuild lands, the codebase carries the airborne-stuck regression #182 introduced.
8. Pointers
- Physics/collision SSOT:
claude-memory/project_physics_collision_digest.md(updated with the #182 + this-investigation banner). - #182 issue:
docs/ISSUES.md. CSphere pseudocode:docs/research/2026-07-07-csphere-collision-family-pseudocode.md. - Retail debugger toolchain + the safe-detach recipe:
memory/project_retail_debugger.md+ CLAUDE.md.