From 80881ed6f14acb6e1cd274a79a0d1b6302bacca8 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 7 Jul 2026 13:07:56 +0200 Subject: [PATCH] docs(#182): crowd-collision investigation outcome + velocity-model rebuild design 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 --- docs/ISSUES.md | 39 +++- ...-physics-update-verbatim-rebuild-design.md | 167 ++++++++++++++++++ tools/cdb/retail-crowd-jump.cdb | 38 ++++ tools/cdb/retail-crowd-jump2.cdb | 28 +++ tools/cdb/retail-crowd-jump3.cdb | 34 ++++ 5 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md create mode 100644 tools/cdb/retail-crowd-jump.cdb create mode 100644 tools/cdb/retail-crowd-jump2.cdb create mode 100644 tools/cdb/retail-crowd-jump3.cdb diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 317b2e1b..0e9a7367 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,13 +46,50 @@ Copy this block when adding a new issue: --- +## #183 — Floating distant scenery: trees from another biome hover in the distance + +**Status:** OPEN +**Severity:** LOW +**Filed:** 2026-07-07 +**Component:** rendering / scenery / streaming + +**Description:** Observed during #182 crowd testing (Holtburg area, `+Acdream`): trees +that appear to belong to a different biome render **floating** in the distance — +detached from the ground, at the wrong Z and/or wrong placement. Distant/far-radius +scenery only; unrelated to the collision work. + +**Root cause / status:** Unknown. Candidates: far-tier streaming placing scenery +before its terrain Z is resolved (procedural scenery Z uses the terrain height at the +cell — a stale/zero height would float it), a biome/terrain-type mismatch selecting +the wrong scene set, or a far-LOD placement offset. Not yet investigated. + +**Files:** likely `src/AcDream.App/Rendering/Wb/` scenery pipeline +(SceneryRenderManager / SceneryHelpers) + the two-tier streamer's far tier. + +**Acceptance:** distant scenery sits on the ground with correct biome/placement. + +--- + ## #182 — Player wedges in a packed monster crowd, can't wiggle free (hand-rolled SphereCollision) -**Status:** DONE (fix landed; pending user visual gate) +**Status:** IN-PROGRESS — CSphere port shipped (`96ae2740`) but **visual-gate FAILED**; +the real fix is a velocity-model rebuild (see design spec), deferred to a new session. **Severity:** MEDIUM **Filed:** 2026-07-07 **Component:** physics / collision +**GATE OUTCOME (2026-07-07):** The CSphere-family port below was faithful but did NOT +fix the crowd feel and INTRODUCED an airborne "stuck in the falling animation" +regression. Player-attributed retail cdb trace proved retail's local client fully runs +player-vs-creature collision (~78% OK, glides) — NOT server-authoritative. Root +divergence: retail's per-frame `UpdateObjectInternal` sets velocity = ACTUAL movement/dt +(blocked → velocity collapses → gravity → glide); acdream integrates+reflects (jump +velocity persists → hang). Fix = verbatim rebuild of the per-frame player-physics loop, +velocity model first. Design: +`docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md`. +#182 stays as the base (user), so the codebase carries the airborne regression until +the rebuild lands. + **Description:** In a large group of monsters packed around the player it was too easy to get stuck — the player couldn't shuffle/slide out. Retail leaves room to wiggle free. diff --git a/docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md b/docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md new file mode 100644 index 00000000..33a8ca9f --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md @@ -0,0 +1,167 @@ +# 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) + +1. **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. +2. **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 of `land_on_sphere=0` was 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` + the + `cached_velocity = (resolved−old)/dt` recompute, 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_collisions` verbatim (once Slice 1 shows what residual the + velocity model leaves) + any remaining `UpdateObjectInternal` orchestration. +- **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=` JSONL of + every player resolve; the `py` histogram (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 via `validate_transition` terminal + top-level `qd`). +- **Conformance tests:** velocity-from-movement (blocked → velocity → ~0); jump + + gravity integration; the `UpdateObjectInternal` sequence; 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) +1. Read `UpdatePositionInternal`, `calc_acceleration` (0x00510950), and + `handle_all_collisions` (0x00514780) fully — confirm the integration + the role of + `handle_all_collisions` vs the cached_velocity model (does retail ALSO reflect, or + is velocity purely movement-derived?). +2. Confirm which `SetPositionInternal` overload the player path uses (3 exist). +3. 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. +4. #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. diff --git a/tools/cdb/retail-crowd-jump.cdb b/tools/cdb/retail-crowd-jump.cdb new file mode 100644 index 00000000..991cd362 --- /dev/null +++ b/tools/cdb/retail-crowd-jump.cdb @@ -0,0 +1,38 @@ +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\retail-crowd-jump.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +r $t0 = 0 +r $t1 = 0 +r $t2 = 0 +r $t3 = 0 + +.echo === ARMED: land_on_sphere + validate_transition + FindObjCollisions; auto-detach after 25000 SetPositionInternal === + +* CSphere::land_on_sphere (0x005379a0) — an AIRBORNE mover's foot sphere hit another +* sphere (a creature body or a sphere-object). If this fires when you jump onto the +* monster pack, the LOCAL client is attempting a creature landing (branch 4 airborne). +* Rare -> always gc, never strands. +bp acclient!CSphere::land_on_sphere "r $t0 = @$t0 + 1; .printf \"[LAND-ON-SPHERE] #%d\\n\", @$t0; gc" + +* CTransition::validate_transition (0x0050aa70) — the commit/revert. arg2 (at esp+4, +* thiscall) is the transitional_insert RESULT fed in: 1=OK 2=COLLIDED 3=ADJUSTED 4=SLID. +* Rate-limited so the log stays readable. A burst of arg2=2 (COLLIDED) during the +* jump-land = the local transition hit the creature and reverted (server must place you). +bp acclient!CTransition::validate_transition "r $t1 = @$t1 + 1; .if (@$t1 % 60 == 0) { .printf \"[VALIDATE] #%d arg2=%d\\n\", @$t1, poi(@esp+4) }; gc" + +* CPhysicsObj::FindObjCollisions (0x0050f050) — count only. Confirms the local player's +* transition is testing object collisions at all during the repro. +bp acclient!CPhysicsObj::FindObjCollisions "r $t3 = @$t3 + 1; gc" + +* CPhysicsObj::SetPositionInternal — the position commit AND the detach anchor. Fires for +* every object's position set (player + creatures), so it always reaches the threshold in +* a live crowd. On the 25000th hit it prints the summary and does NOT gc -> cdb breaks -> +* g returns -> the top-level qd detaches cleanly. (qd is NEVER inside a bp action.) +bp acclient!CPhysicsObj::SetPositionInternal "r $t2 = @$t2 + 1; .if (@$t2 < 25000) { gc } .else { .printf \"[SUMMARY] land_on_sphere=%d validate=%d findObjColl=%d setPos=%d\\n\", @$t0, @$t1, @$t3, @$t2 }" + +g +.echo === DETACHING === +qd +.logclose diff --git a/tools/cdb/retail-crowd-jump2.cdb b/tools/cdb/retail-crowd-jump2.cdb new file mode 100644 index 00000000..d050d5f3 --- /dev/null +++ b/tools/cdb/retail-crowd-jump2.cdb @@ -0,0 +1,28 @@ +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\retail-crowd-jump2.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +r $t0 = 0 +r $t1 = 0 +r $t2 = 0 + +.echo === ARMED v2: land_on_sphere + validate(COLLIDED count); AUTO-DETACH after 6000 validate === + +* CSphere::land_on_sphere (0x005379a0) — an airborne mover's foot sphere hit another +* sphere. RARE -> cheap, no meaningful lag. $t1 (validate count) timestamps each hit so we +* can read the PATTERN: a brief burst per jump = the local client lands on the creature; +* a long steady burst = it's stuck locally (arming every frame) and the SERVER lifts you. +bp acclient!CSphere::land_on_sphere "r $t0 = @$t0 + 1; .printf \"[LAND] #%d vt=%d\\n\", @$t0, @$t1; gc" + +* CTransition::validate_transition (0x0050aa70, UNAMBIGUOUS) — the commit/revert, AND the +* detach anchor. arg2 (poi esp+4) = the transitional_insert result: 1=OK 2=COLLIDED 4=SLID. +* We count COLLIDED and log every 5th so a jump that reverts locally shows a COLLIDED spike. +* On the 6000th hit (~15-20 s) it prints the summary and does NOT gc -> cdb breaks -> g +* returns -> the top-level qd detaches cleanly. qd is NEVER inside a bp action. +bp acclient!CTransition::validate_transition "r $t1 = @$t1 + 1; .if (@$t1 < 6000) { r $t3 = poi(@esp+4); .if (@$t3 == 2) { r $t2 = @$t2 + 1; .if (@$t2 % 5 == 0) { .printf \"[COLLIDED] #%d vt=%d\\n\", @$t2, @$t1 } }; gc } .else { .printf \"[DETACH] validate=%d land_on_sphere=%d collided=%d\\n\", @$t1, @$t0, @$t2 }" + +g +.echo === DETACHING === +qd +.logclose diff --git a/tools/cdb/retail-crowd-jump3.cdb b/tools/cdb/retail-crowd-jump3.cdb new file mode 100644 index 00000000..57281627 --- /dev/null +++ b/tools/cdb/retail-crowd-jump3.cdb @@ -0,0 +1,34 @@ +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\retail-crowd-jump3.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +r $t9 = 0 +r $t0 = 0 +r $t1 = 0 +r $t2 = 0 +r $t4 = 0 +r $t5 = 0 + +.echo === ARMED v3 (PLAYER-ATTRIBUTED): capture player ptr from set_local_velocity; filter validate + land_on_sphere to it; auto-detach after 40000 validate === + +* Capture the LOCAL player's CPhysicsObj* from the first input-driven local-velocity set. +* Creatures are dead-reckoned (interpolated), not set via set_local_velocity, so the first +* hit after you start moving is the player. Printed so we can sanity-check it's one pointer. +bp acclient!CPhysicsObj::set_local_velocity "r $t8 = @ecx; .if (@$t9 == 0) { r $t9 = @$t8; .printf \"[PLAYER-PTR] %p\\n\", @$t9 }; gc" + +* land_on_sphere: mover = arg2->object = poi(poi(esp+4)) (OBJECTINFO.object at offset 0). +* Log ONLY when the mover is the player. If this stays 0 while you jump onto monsters, the +* local client is NOT computing your creature landing (server places you). +bp acclient!CSphere::land_on_sphere "r $t7 = poi(poi(@esp+4)); .if (@$t7 == @$t9) { r $t0 = @$t0 + 1; .printf \"[PLAYER-LAND] #%d vt=%d\\n\", @$t0, @$t1 }; gc" + +* validate_transition: mover = poi(ecx) (CTransition.object_info.object at offset 0). For the +* PLAYER only, tally the transition result arg2 (poi esp+4): 2=COLLIDED, 4=SLID, else OK/ADJ. +* pCOL/pSLID near 0 during the crowd jump = the player is NOT locally colliding with creatures. +* $t1 (ALL validates) is the unambiguous detach anchor -> break at 40000 -> qd. +bp acclient!CTransition::validate_transition "r $t1 = @$t1 + 1; .if (@$t1 >= 0n40000) { .printf \"[DETACH] pPtr=%p pValidate=%d pCOLLIDED=%d pSLID=%d pLand=%d\\n\", @$t9, @$t2, @$t4, @$t5, @$t0 } .else { .if (poi(@ecx) == @$t9) { r $t2 = @$t2 + 1; r $t3 = poi(@esp+4); .if (@$t3 == 2) { r $t4 = @$t4 + 1 } .else { .if (@$t3 == 4) { r $t5 = @$t5 + 1 } } }; gc }" + +g +.echo === DETACHING === +qd +.logclose