feat(physics): #32 L.5 30Hz physics tick + retail debugger toolchain (#35) + Phase 3 retail-faithful kill_velocity

Three intertwined changes from a single investigation session driven by
attaching cdb to a live retail acclient.exe (v11.4186, Sept 2013 EoR
build) and tracing what retail actually DOES on the steep-roof wedge
scenario the user reported in acdream.

═══════════════════════════════════════════════════════════
1. L.5 — physics-tick MinQuantum gate (PlayerMovementController)
═══════════════════════════════════════════════════════════

Retail's CPhysicsObj::update_object subdivides per-frame dt into 1/30 s
sized integration steps and SKIPS entirely when accumulated dt is below
MinQuantum. Live trace evidence:

  update_object        = 40,960 calls
  UpdatePhysicsInternal = 25,087 calls   (61%)

i.e., 39% of update_object calls return early via the MinQuantum gate.
Retail's effective physics tick rate is 30Hz even at 60+ Hz render.

acdream's PlayerMovementController bypassed the existing PhysicsBody.
update_object and called UpdatePhysicsInternal(dt) directly each render
frame, which compressed bounce-energy / gravity-tangent accumulation
into half the time and amplified our steep-roof wedge dynamics.

Fix: add `_physicsAccum` accumulator. Integrate only when accumulated
dt ≥ MinQuantum (clamped to MaxQuantum to bound stale-frame jumps).
HugeQuantum drops accumulated time to discard truly stale frames
(debugger break, GC pause). Render still runs at full rate; only the
physics step is gated.

═══════════════════════════════════════════════════════════
2. Phase 3 reset retail-faithful kill_velocity (TransitionTypes)
═══════════════════════════════════════════════════════════

Retail's reset path (acclient_2013_pseudo_c.txt:273231-273239) gates
kill_velocity on `last_known_contact_plane_valid`:

  if (last_known_valid == 0) {
      set_collision_normal(step_up_normal); return COLLIDED;
  }
  kill_velocity(this);
  last_known_valid = 0;
  return COLLIDED;

Earlier in this session I deviated to "unconditional kill_velocity" as
a hypothesis-driven wedge fix. The live trace then showed the
deviation CAUSED a different wedge by zeroing V every frame, leaving
the body with no tangent momentum to escape (V = (0,0,0) for 169
consecutive frames while position pre/resolved frozen). The retail-
faithful gate is restored.

Note: the gate rarely fires in normal airborne play because our L.2.4
proximity guard clears last_known_valid soon after the body separates
from its remembered floor. Live retail trace also showed
kill_velocity = 0 hits over an entire play session — same behavior. So
acdream's kill_velocity is correct as ported now.

The supporting ObjectInfo.VelocityKilled flag + StopVelocity wiring +
PhysicsEngine.ResolveWithTransition consumer that actually zeros
body.Velocity when the flag is set — these were a no-op stub before
this session and are now correctly wired. Retail anchor:
OBJECTINFO::kill_velocity → CPhysicsObj::set_velocity({0,0,0}, 0) at
acclient_2013_pseudo_c.txt:274467-274475.

═══════════════════════════════════════════════════════════
3. Retail debugger toolchain (#35)
═══════════════════════════════════════════════════════════

When the question is "what does retail actually DO at runtime?" — not
"what does retail's code SAY" — the decomp at docs/research/named-retail/
is invaluable but doesn't capture state interactions across frames.
This commit ships infrastructure to attach Windows' cdb.exe to a live
retail acclient.exe with full PDB symbols and capture state at any
breakpoint.

  - tools/pdb-extract/check_exe_pdb.py — reads any PE's CodeView entry
    and reports MATCH / MISMATCH against refs/acclient.pdb's GUID.
    Always run before attaching cdb. The matching v11.4186 build's
    GUID is 9e847e2f-777c-4bd9-886c-22256bb87f32.

  - tools/pdb-extract/dump_pdb_info.py — dumps a PDB's expected
    build timestamp + GUID + age. Used to figure out which acclient.exe
    build pairs with our PDB.

CLAUDE.md gets a Step -1 in the development workflow ("ATTACH cdb
TO RETAIL when behavior is the question, not code") and a full
"Retail debugger toolchain" section with the workflow, sample .cdb
script structure, and watchouts (PDB names use snake_case for some
classes / PascalCase for CPhysicsObj; ; is cdb's command separator;
killing cdb kills the debuggee; high-hit-rate breakpoints lag the game).

memory/project_retail_debugger.md captures the workflow + key findings
so future sessions inherit the toolchain by reading project memory.

═══════════════════════════════════════════════════════════
4. BSPQuery Path 6 slide-tangent restored (b1af56e behavior)
═══════════════════════════════════════════════════════════

After this session's retail-strict experiments showed that retail-
faithful Path 6 (SetCollide + Phase 3 reset chain) produces a
"lands on roof in falling animation, can't slide off" half-state in
acdream — because our acdream port of step_up_slide / cliff_slide is
incomplete for grounded-on-steep movement — the L.4 slide-tangent
deviation from commit b1af56e is restored as the pragmatic ship state.

The deviation: when an airborne sphere hits a polygon whose normal Z
is below FloorZ (≈ 0.6642, slope > ~49°), project the move along the
steep face to remove the into-wall displacement, set CollisionNormal +
SlidingNormal, return Slid. Body never gets ContactPlane on the steep
poly, never gets the half-state, slides off the slope under gravity's
tangent contribution.

Retail-strict requires the deeper step_up_slide / cliff_slide audit
(filed under #32). Until that lands, slide-tangent is the right
deviation — produces user-acceptable "slide off the roof" behavior.

═══════════════════════════════════════════════════════════
Test status: 833/833 green.

Refs:
  acclient_2013_pseudo_c.txt:283950 (CPhysicsObj::update_object)
  acclient_2013_pseudo_c.txt:273231-273239 (Phase 3 reset path)
  acclient_2013_pseudo_c.txt:274467-274475 (OBJECTINFO::kill_velocity)
  acclient_2013_pseudo_c.txt:323783-323821 (BSPTREE::find_collisions Path 6)

Closes #35. Updates #32 with L.4/L.5 status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-30 22:41:12 +02:00
parent b1af56eb19
commit 235de3322a
8 changed files with 624 additions and 82 deletions

View file

@ -196,11 +196,44 @@ before `SPHEREPATH::precipice_slide`; edge-slide `Slid` / `Adjusted` results
now feed the `TransitionalInsert` retry loop instead of being reverted by outer
validation, and a synthetic diagonal terrain-boundary test covers tangent
motion. `ACDREAM_DUMP_EDGE_SLIDE=1` now reports whether a failed step-down had
polygon context. Remaining gaps: live visual confirmation of the retry-loop
fix, real-DAT building-edge fixtures, fuller `cliff_slide` coverage, and
`NegPolyHit` dispatch. Named retail anchors include `CTransition::edge_slide`,
`CTransition::cliff_slide`, `SPHEREPATH::precipice_slide`, and
`SPHEREPATH::step_up_slide`.
polygon context.
**L.4/L.5 update 2026-04-30:** A retail debugger trace (cdb attached to
v11.4186 acclient.exe — see #35) confirmed that retail does NOT wedge
on the steep-roof scenario that produces the wedge in our acdream port.
Three concrete findings:
1. Retail's `OBJECTINFO::kill_velocity` rarely fires in normal play —
gated on `last_known_contact_plane_valid`, which our L.2.4 proximity
guard tends to clear before steep-poly hits land. Retail trace: 0
kill_velocity hits across 40,960 update_object calls. Our Phase 3
reset path now matches retail's gate (only kills when valid).
2. Retail integrates physics at 30Hz (`MinQuantum = 1/30 s`); render is
60+ Hz. UpdatePhysicsInternal/update_object ratio = 0.61. We
ported this gate as L.5 in `PlayerMovementController` via
`_physicsAccum`. Render still runs at 60+ Hz; only the physics
integration step is 30Hz.
3. The remaining wedge cause — body's pre-position drifts to the
polygon's tangent and gravity's tangent component into surface
produces a stable retain-collide-revert loop — is a downstream
consequence of retail's grounded-on-steep escape chain
(`step_sphere_up``step_up_slide``cliff_slide`) being
incompletely ported. Live test confirmed retail-strict Path 6
produces "lands on roof in falling animation, can't slide off"
half-state because that chain doesn't produce smooth descent.
**Pragmatic ship-state:** BSPQuery Path 6 keeps the L.4 slide-tangent
deviation (project-along-steep-face-and-return-Slid) for steep-poly
airborne hits. It produces user-acceptable "slide off the roof"
behavior at the cost of departing from retail's Path 6 → SetCollide →
Path 4 → Phase 3 reset chain. Retail-strict requires the
step_up_slide / cliff_slide audit below; until that lands, slide-tangent
is the right deviation.
Remaining gaps: real-DAT building-edge fixtures, fuller `cliff_slide`
coverage, `NegPolyHit` dispatch, and the retail-strict
step_up_slide / cliff_slide audit (filed for follow-up). Named retail
anchors include `CTransition::edge_slide`, `CTransition::cliff_slide`,
`SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`.
**Files:** `src/AcDream.Core/Physics/TransitionTypes.cs`,
`src/AcDream.Core/Physics/BSPQuery.cs`,
@ -214,6 +247,52 @@ cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case.
---
## #35 — [DONE 2026-04-30] Retail debugger toolchain (cdb + PDB GUID matching)
**Status:** DONE
**Severity:** N/A (infrastructure)
**Filed + closed:** 2026-04-30
**Component:** tooling / research
**Description:** When the question is "what does retail actually DO at
runtime?" — wedges, animation flicker, geometry-specific bugs where the
decomp is correct but the visible behavior is mysterious — there was no
way to attach a debugger to a live retail acclient.exe and trace it.
This issue tracks the toolchain that closed that gap.
**What shipped:**
- **`tools/pdb-extract/check_exe_pdb.py`** — reads any PE's CodeView entry
and reports `MATCH` / `MISMATCH (expected GUID = …)` against our
`refs/acclient.pdb`. Always run before attaching cdb.
- **`tools/pdb-extract/dump_pdb_info.py`** — dumps a PDB's expected
build timestamp + GUID + age. Used to figure out which acclient.exe
build pairs with our PDB (answer: v11.4186, Sept 2013 EoR).
- **CLAUDE.md "Retail debugger toolchain" section** — full workflow:
cdb path, sample `.cdb` script, PowerShell wrapper pattern, watchouts
(PDB name conventions, `;` parsing, kill-target-on-detach behavior,
high-hit-rate lag).
- **Step `-1` added to the development workflow** — "ATTACH cdb TO
RETAIL (when behavior is the question, not code)". Tells future
sessions: when guessing has failed twice in a row, don't keep guessing.
**Discoveries this toolchain enabled (closed in same session):**
- Retail integrates physics at 30Hz (`UpdatePhysicsInternal/update_object`
ratio = 0.61). Drove the L.5 fix in PlayerMovementController.
- `OBJECTINFO::kill_velocity` rarely fires in normal play (gated on
last_known_contact_plane_valid). Our acdream port now matches.
- Retail does NOT wedge on the steep-roof scenario. Confirmed our L.4
slide-tangent deviation in Path 6 is necessary until the retail
step_up_slide / cliff_slide chain audit lands.
**Files:** `tools/pdb-extract/check_exe_pdb.py`,
`tools/pdb-extract/dump_pdb_info.py`, `CLAUDE.md`,
`memory/project_retail_debugger.md`.
**Acceptance:** Future sessions can attach cdb to a live retail client
in under 5 minutes by following the CLAUDE.md workflow.
---
## #33 — Live entity collision shape collapses to one cylinder
**Status:** OPEN