acdream/docs/plans/2026-07-29-physics-parity-campaign.md

24 KiB
Raw Blame History

Campaign P — Physics Retail-Feel Parity

Status: CLOSED 2026-07-31 — final user matrix accepted; tight-gap clearance issue #273 and deferred restricted-house gate #274 are explicitly carried follow-ups.

Filed: 2026-07-29. Directed by the user as a pre-vendor-management detour after the same-day physics/collision retail-fidelity audit. The world-interaction program (Slice 5, vendor browsing) resumes when this campaign closes.

Execution model: Claude drives autonomously slice-to-slice. Sonnet subagents implement bounded chunks against this plan's specs; an Opus review subagent gates every slice boundary. The only user stops are (a) the final batched connected visual matrix, (b) a DO-NOT-RETRY conflict, (c) anything destructive. All the CLAUDE.md workflow rules apply: grep-named-first → pseudocode → port → conformance test; register moves in the same commit; no workarounds.


The parity goal (the autonomy contract)

"Retail Movement Parity v1" — the campaign is DONE when all of the following are auditable-true:

  1. Zero physics-scope temporary stopgaps. TS-1, TS-4, TS-5, TS-23, TS-46 retired by porting the retail mechanism. TS-24, TS-35, TS-40 either retired or re-classified (IA/AD) with a recorded justification.
  2. No feel-affecting approximations left unargued. AP-7 resolved by decoding retail's friction state gate; AP-10 restored to retail's 0.1 m water sink-in; AP-25 replaced by the retail effective-skill chain (vitae/enchantment-aware); AP-71 (check_entry_restrictions) ported.
  3. Issue ledger: #262, #165, #166, #116, #167, #72, #153 closed; the stale "pending visual gate" statuses on #172/#173/#174/#175/#41 reconciled (folded into the final matrix below). Explicitly excluded: #235 (user-deferred 2026-07-27) and #256/#257 (lifecycle/memory, not physics feel — separate track).
  4. Verification: each port carries decomp citations + conformance tests; dotnet build + full Release suite green at every slice commit; one batched connected visual matrix (below) passes at the end, run by the user.

Anything not in this list is out of scope for the campaign — file it, don't chase it.


Slices

P1 — Stat-coupled movement (burden / stamina / vitae) — retires TS-5, AP-25, the burden gap

Status (2026-07-30): COMPLETE. Landed at 9355ddce; Opus review APPROVE at 001e466d (which also retired UN-8 — the CanJump polarity is byte-PROVEN load < 2.0 from the PDB-paired binary — and recorded the PK-timer jump-cost decode for P3). Full Release suite 9,880/0/5 at the slice gate. Retail's vitae/enchant chain reuses the M3 bucket-4 representation; JumpStaminaCost never refuses (weak-jump only) — the plan's formula shorthand had the +0.5 operand wrong and the implementation follows the decomp's (load+0.5)*power*8+2. AP-127 was filed for the then-bounded bonus properties and retired by #268 on 2026-07-31.

Today PlayerWeenie.SetBurden has zero callers, CanJump is always true, JumpStaminaCost is 0, and pushed run/jump skill is attributeBonus + init + ranks only. Retail modulates movement by character state continuously.

Retail anchors (named decomp, verified 2026-07-29):

  • ACCWeenieObject::{CanJump 0x0058c400, JumpStaminaCost 0x0058c440, InqJumpVelocity 0x0058c520, InqRunRate 0x0058c560, InqMaxRunRate 0x0058c5a0} — thin delegations to the qualities DB (pseudo-C ~406512).
  • CACQualities::{InqMaxRunRate 0x00591b20, CanJump 0x00591b50, JumpStaminaCost 0x00591b90, InqRunRate 0x00592800, InqJumpVelocity 0x00592980, InqLoad 0x0058f130} (pseudo-C ~412901413975, ~409756) — the real load/skill/vitae composition. InqJumpVelocity ends in sqrt(GetJumpHeight(...) * 19.6) (pc 413975).
  • MovementSystem::{GetRunRate 0x006b0950, GetJumpHeight 0x006b09b0, JumpStaminaCost 0x006b0a40} (pseudo-C ~695958) — GetJumpHeight readable: LoadMod(load) * (skill/(skill+1300) * 22.2 + 0.05) * power / scaling, floor 0.35; JumpStaminaCost readable: ceil(((power + 0.5) * load) * 8 + 2) on the arg3==0 branch.
  • EncumbranceSystem::{EncumbranceCapacity 0x004fcc00, Load 0x004fcc40, LoadMod 0x004fcc70} (pseudo-C ~256393).
  • Cross-refs: ACE MovementSystem/EncumbranceSystem C# ports; holtburger if it models load.

Work:

  1. Port the full chain into Core (EncumbranceSystem + MovementSystem statics; PlayerWeenie becomes the CACQualities-shaped composition). Where BN x87 mush blocks a branch (GetRunRate body), use Ghidra MCP or the ACE port as the tiebreaker and cite which.
  2. Determine, from CACQualities::InqRunRate's own body, exactly which skill level retail feeds (base vs enchantment/vitae-adjusted) and port THAT — scoped to the run/jump query path only, reading vitae + relevant skill enchantments from the M3 active-effect state. Do not build a general effective-skill engine.
  3. Plumb the inputs from Runtime: burden (EncumbranceVal/capacity from PlayerDescription + property updates), current stamina (vitals), vitae. Extend RuntimeMovementSkillState (J4.4 seam) so updates flow mid-session, same as run/jump skill today.
  4. Wire the existing jump_is_allowed stamina-refusal branch and the ReportExhaustion dual-dispatch gate (R3-W4 seam) to a real consumer, matching retail's refusal/weak-jump behavior.
  5. Conformance tests: formula tables (golden values incl. 800-skill cap, load-mod knees at 100%/200%, stamina cost ceil), gating tests (no-stamina jump refusal), plumbing tests (burden/stamina/vitae changes move the produced rate). Register: delete TS-5 + AP-25 rows, note the retirement in the same commit.

P2 — Response-layer edge family — retires TS-1, TS-4, AP-7; closes #166, #116

Status (2026-07-30, FINAL — Campaign P final physics slice): TS-1 and AP-7 retired same-day as originally recorded. TS-4 is now ALSO RETIRED — the oracle follow-up pass (docs/research/2026-07-30-ts4-116-oracle-plan.md) found the freeze the first implementation attempt hit was one layer downstream of Path 6 (inside AdjustOffset's crease projection against a purely-vertical offset — a genuine retail-identical degeneracy, not a bug) and ran the plan's own decisive confirming test: Ts4SteepRoofWedgeCaptureTests's horizontal-velocity variant (matching the realistic live-play input that originally validated the shortcut) converges cleanly with the shortcut removed. The Path-6 steep-poly shortcut is deleted from both BSPQuery.cs and FlatBspQuery.cs; the pure-vertical degenerate case is pinned (not fixed) as register row AD-56. #116 is narrowed, not closed: shape-2 (D4 first-airborne-frame hard-stop) is CLOSED — the oracle plan's structural dispatch-routing hypothesis was confirmed by instrumentation with no cdb session needed, and the D4 pin is un-skipped. Shape-1 (tick-22760 lateral-slide loss) got a real, independently-decomp- confirmed fix (Path 6's foot-clear/head-hit branch now returns Collided

  • SetCollisionNormal directly, matching pc:323824-323834/ACE BSPTree.cs:221-230), but the confirming replay showed this does NOT explain tick-22760 itself — that mover is grounded (dispatches through Path 5, not Path 6) and the actual "no normal recorded" mechanism (SpherePath.PrecipiceSlide's find_crossed_edge-false fallback) is independently confirmed byte-exact retail behavior too. The remaining divergence is most likely this test's simplified door-registration fixture, not the response layer — see ISSUES.md #116 and the oracle plan's Addendum 2 for the full trace and the concrete next step (re-run against the faithful Setup-based door registration). AD-55 (the sled slope-flatness constant, split out of AP-7's retirement) is ALSO retired this same slice — byte-proven cos(10°) per the oracle plan's Addendum.

The collision response layer (what happens after a hit): ground friction, cliff edges, downhill landings, near-perpendicular wall slides. One oracle-driven pass; the physics digest's DO-NOT-RETRY table binds every subagent here.

Work (research doc FIRST, then port):

  1. AP-7: decode the state gate on retail's friction block (calc_friction region, pseudo-C ~276702-276705) that lets retail use threshold 0.25 without hammering normal locomotion (the reverted L.3c attempt). Ghidra MCP for the x87 branch if BN is garbled.
  2. TS-1: port the EdgeSlide → PrecipiceSlide / CliffSlide chain (precipice context, steep-plane bookkeeping) replacing our stop-at-edge.
  3. #166: port the landing "sled" (Sledding state set/clear sites; the sled friction constants already sit in calc_friction).
  4. TS-4: replace the Path-6 steep-poly in-place-slide shortcut with retail's SetCollide → Path-4 → ContactPlane landing chain, and remove the two BSP-layer SetSlidingNormal writes (retail's only in-transition writer is validate_transition).
  5. #116: the near-perpendicular lateral-slide loss + first-airborne- frame divergence, driven by the existing tick-22760 replay and D4 pins.
  6. Apparatus: extend the trajectory-replay tests; capture fixtures before changing behavior. Register: delete TS-1/TS-4/AP-7 rows.

P3 — Remote-object residuals — retires TS-46, TS-23; closes #165; narrows/retires AD-25

  1. TS-46: pass the Setup's verbatim sphere LIST into the transition (CPhysicsObj::transition 0x00512dc0 → init_sphere) instead of the two-scalar reconstruction, for local player and remotes; derive remote step-up/step-down from the Setup instead of the pinned 0.4 m. Captured-fixture replays must stay green or be re-baselined with evidence.
  2. AD-25: align the remote post-resolve with the ported handle_all_collisions (grounded-bounce rule) as the player half already did in #182.
  3. #165: remotes visibly penetrate walls before stopping — diagnose against the (now Setup-true) sweep; suspect list starts at the catch-up step length vs sweep sub-steps.
  4. TS-23: parse PlayerKillerStatus from PlayerDescription/property updates and plumb PK/PKLite/Impenetrable onto local + remote player movers (OBJECTINFO::init 0x0050cf30 state bits). Non-PK ACE behavior must be provably unchanged.

P4 — World specials — retires AP-71, AP-10

Status (2026-07-30): COMPLETE, including a same-day Opus review fix. AP-71 landed at d6c3f865 (20 new conformance tests); AP-10 landed at cc8d57a2 (12 new conformance tests). Complete solution suite at that gate: 9,946 total across 9 test projects, 9,941 passed, 5 skipped, 0 failed on a clean run. One run in the same session saw a single unrelated flake (AcDream.Content.Tests.Vfx.RetailDatLoaderTests .AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel, a parallel-cache-coalescing timing test untouched by either commit) that passed 3/3 in isolation and on the immediate re-run — full-suite parallel contention, not a regression (independently fixed afterward at dc0468cc).

P4 review verdict: FIX-FIRST (2026-07-30). RestrictionObjPrevalenceInspectionTests (3b5e0992) measured the installed cell DAT: 103,766 of 729,888 EnvCells across 1,293 landblocks — the entire housing estate, restrictionObj GUIDs 0x70xxxxxx — carry a baked RestrictionObj. AP-71's fail-closed default (landed with CanMoveInto deliberately unmodeled, per the original AP-129 row) would have locked every apartment/cottage/villa interior for every player, including its own owner — a live regression, not the "inert in dev content" the row assumed. Fixed at 7a0f836a: ACCWeenieObject::CanMoveInto (0x0058da40) and RestrictionDB::IsAllowedIn (0x005ae8f0) are now ported verbatim, fed end-to-end from CreateObject's HouseOwner/HouseRestrictions/ Monarch PWD-tail fields (previously parsed-and-discarded) plus a new live House_UpdateRestrictions (0x0248) parser, and resolved through a new PhysicsEngine.Objects property wired to the canonical ClientObjectTable in RuntimeEntityObjectLifetime (production fix, not just gate logic — an unwired table still fails closed). AP-129 is narrowed (not retired) to the genuine residual: no sequence-based staleness rejection for House_UpdateRestrictions (low-probability, self-correcting), and the outdoor CLandCell restriction path (a separate DAT structure) remains unported, unaffected by this fix. Gate: AcDream.Core.Tests 4,049 passed / 2 skipped / 0 failed; AcDream.Core.Net.Tests 761 passed / 0 skipped / 0 failed; complete solution suite 9,961 total, 9,956 passed, 5 skipped, 0 failed.

  1. AP-71: port the CObjCell::check_entry_restrictions gate at the head of find_env_collisions (pc:309576) — barred house cells block at the threshold client-side. Landed: the gate is wired at the top of the indoor branch of Transition.FindEnvCollisions; CellPhysics .RestrictionObj is fed from the DAT-baked EnvCell.RestrictionObj field (§4.3's open question resolved via ACE's DatLoader + an independent Chorizite.DatReaderWriter reflection probe — it's a plain per-cell DAT field, not a live wire override) in both the dev and production caching paths, at zero bake-format cost. The mover's CanBypassMoveRestrictions (BF_ADMIN & BF_IMMUNE_CELL_RESTRICTIONS) is decoded via the TS-23 PWD-bitfield pipeline. The original landing deliberately left CanMoveInto unmodeled (fail-closed default, filed as AP-129) — the P4 review found this fails closed for the ENTIRE housing estate and required the fix-first pass described above.
  2. AP-10: restore retail's 0.1 m water sink-in; while there, verify the water-contact step behavior (WATER_CONTACT_TS consumers) against retail and file anything found. Landed: the dry-corner constant is restored (full suite green — the sticky Contact/OnWalkable bit argument held); WaterContact is now produced at every Contact/OnWalkable commit site. No confirmed retail consumer of WATER_CONTACT_TS was found this pass; filed as #264 along with two other explicitly-unverified water items (the ENTIRELY_WATER ethereal/swim terrain-collision exemption, and jump/swim movement-effects) — none block this port.

P4 review addendum (2026-07-30): APPROVED after one FIX-FIRST round. The initial AP-71 landing failed closed with CanMoveInto unmodeled; the prevalence inspection (3b5e0992) proved that locks all 103,766 housing EnvCells. 7a0f836a ports CanMoveInto/IsAllowedIn verbatim (owner/self/null-db admit; unresolved object blocks), captures the previously-discarded HouseOwner/Monarch PWD fields, parses House_UpdateRestrictions 0x0248 live, and wires the canonical object table into the gate. AP-129 narrowed to the sequence-byte and outdoor RestrictionTables residuals. Suite 9,956/0/5.

P5 — Deferred fidelity — closes #167, #153, #72

Status (2026-07-30): item 1 (#167) COMPLETE. Both blockers resolved without Ghidra/cdb — the two x87-elided constants were byte-decoded straight from the matching binary's raw machine code (docs/research/2026-07-30-constraint-leash-constants.md). The leash is now armed at every current acdream inbound-position acceptance seam (ConstraintDistance, LiveEntityNetworkUpdateController, PlayerMovementController.SetPosition/BlipPosition), the per-tick PhysicsBody.IsFullyConstrained push replaces the always-false stub, and register row TS-35 is deleted. Full Core/Runtime/App suites pass with new conformance tests (leash-armed jump refusal, teleport-vs-blip anchor/teardown, taper reduction, remote-tick push). Items 2 (#153) and 3 (#72) remain open.

  1. #167: decode the two unknown x87 ConstraintManager constants (Ghidra) and port leash arming.
  2. #153: the far-teleport arrival onto an unstreamed landblock near a 192 m edge — apparatus first (the issue's own trigger table), then the streaming-gap hold shape ALREADY sketched there (freeze the per-tick resolve until the landblock loads — the async equivalent of retail's synchronous load; this is an AD row, not a workaround, and gets filed as one).
  3. #72: close on the R6 evidence (DAT-authored omega ±1.5 rad/s is live; the cdb confirmation ask is obsolete).

P6 — #262 login run-on-the-spot (live defect)

Probe-instrumented fresh-process login repros (ACDREAM_PROBE_RESOLVE=1

  • net probes) against local ACE; the issue's hypothesis list is the script. Root cause, fix, regression test. No workarounds (no auto-recall, no synthetic position kick). Runs serialized (owns the build tree + client).

P7 — Ledger + camera feel

  1. Retire the stale TS-25 row (outbound stance ships via RawState.CurrentStyle since #219) and refresh TS-24/TS-35/TS-40 classifications.
  2. #115 camera-drag: investigation-only against CameraManager constants (AD-37's vector-nlerp vs retail quaternion-slerp is the prime suspect); fix if a concrete divergence falls out, otherwise re-classify with evidence.
  3. Reconcile #172/#173/#174/#175/#41 statuses via the final matrix.

Implementation-phase closeout (2026-07-30) — awaiting the matrix

Every implementation slice is COMPLETE and Opus-reviewed; the campaign now waits on the single user gate below.

Register scorecard: goal-enumerated physics stopgaps at ZERO — TS-1, TS-4 (+ its FlatBspQuery twin), TS-5, TS-23, TS-35, TS-46 retired by ports; TS-25 retired on #219 evidence; TS-24→AD-57, TS-40→AD-58 re-argued. AP-7, AP-10, AP-25, AP-71 retired; UN-8 and AD-55 retired by raw-byte proof; AD-25 retired. AP-127 was subsequently retired by #268; the remaining new argued rows are AP-128/129, AD-53/54/55(retired)/56/57/58.

Issues: #72, #153, #167, #255 closed; #116 shape-2 closed /shape-1 narrowed to a probable harness artifact (response layer byte-verified); #165 diagnosed to the render-lag candidate (matrix scenario 8a decides); #166 all four composite deviations landed (scenario 5 decides); #262 apparatus permanently live + 3/3 clean probe logins (scenario 11 decides). Notable finds along the way: ACE's inverted leash-start mapping, ACE's radians/degrees sled-constant bug (cos 10°), the housing-lockout prevalence catch (103,766 restricted cells), and the HouseOwner/Monarch PWD fields that were parsed-and-discarded.

Verification: every slice gated on the complete Release suite; final state 9,977 passed / 0 failed / 4 skipped (the D4 un-skip retired one permanent skip). Suite grew from 8,826 to 9,977 tests over the campaign (+1,151, all conformance/golden/pin coverage).

Final batched connected visual matrix (the ONE user gate)

  1. Burden >100%: run slows, jump shrinks; ~200%: barely moves/jumps.
  2. Repeated jumps drain stamina; low stamina → weak/refused jump; exhaustion behavior matches retail.
  3. Fresh vitae: movement penalty present.
  4. Walk off a cliff/roof edge: slides over like retail, no dead stop.
  5. Downhill jump landing: sled glide + bounce.
  6. Shallow-angle wall graze: lateral slide preserved.
  7. Packed crowd: spacing + shuffle-out unchanged (regression).
  8. Two-client: remote stops at walls without visible penetration; remote ceiling-jump bounces down immediately (#173); Holtburg portal platform step-up (#172); door Use after jumping (#174); closed-door collision matches the visual door (#175); observed-player blips gone (#41).
  9. Locked/barred house: blocked at the threshold.
  10. Wading: slight retail sink-in.
  11. ~20 fresh logins: no run-on-the-spot.
  12. Regression sweep: walk/run/strafe/turn/jump/stairs/doors/water edges feel unchanged from the accepted R6 baseline.

Risk notes

  • P2 and P3 touch the frozen-adjacent transition internals — every subagent prompt must carry the digest's DO-NOT-RETRY table and the no-workarounds rule; 3 failed attempts on any item = stop and build apparatus, per feedback_apparatus_for_physics_bugs.
  • TS-46 changes the collision capsule of every mover; the captured replay fixtures pin behavior — re-baseline only with a recorded retail argument.
  • P1's enchantment-aware skill read is the scope-creep risk; it is bounded to the run/jump query path by this plan.

Live-gate session 2 (2026-07-30) — speed + bounce family landed

The matrix's first live rows surfaced three defects; all three are root-caused, retail-ported, and user-accepted in the same session:

  • #266 CLOSED — run speed: retail MovementSystem::GetRunRate (0x006b0950) treats 800 as an EXACT-EQUALITY sentinel; ACE's >= 800 reading is a misread of the same x87 mush that our P1 port inherited, flat-lining every maxed character at 4.5 (retail-true ~3.70, +21%, vitae-independent). Byte-decoded, fixed at 61e95916; the [stat-chain] live capture proved the vitae→skill chain correct end-to-end. Side-by- side pace vs a retail client accepted by the user.
  • #265/#166 landing-momentum + bounce family — two stacked fixes: (1) c60f6e5d stopped hand-zeroing grounded residual velocity and wired the never-written GroundNormal (roof slides restored); (2) 2d611b2b replaced the AD-25 landing adaptation with the retail mechanism: check_contact (0x0050f5b0) transition seeding, the velocity-free SetPositionInternal commit (0x00515330), and the live 5%-elasticity landing reflect (DEFAULT_ELASTICITY 0.05 @0x007c6a7c). Downhill bounce chain, flat-ground pop, and clean uphill landings all user-accepted ("almost pass with merits"). Investigation + byte-decode record: docs/research/2026-07-30-landing-bounce-family.md.
  • #267 shipped (vitae/buff panel values; attributes vitae-immune). #268 closed 2026-07-31: the complete augmentation chain is shared by panel and Runtime movement; AP-127 is retired. Attributes, secondary attributes, and skills use retail's vitae-excluded green/red comparison. The selected-skill footer now renders per-fragment colors through the shared retained text primitive, using the authored 0x1B palette exactly: #7FFFFF vitae, #00FF00 buff, #FF0000 debuff. TS-8 is also retired: a real live 0x02C2 payload carries its full StatMod through dispatch and changes the effective skill immediately. The user accepted the live colors, values, footer, and immediate row refresh.
  • #269 closed 2026-07-31 — the live 2,184-quantum trace proved the landing reflect and friction math were correct. ACDream omitted retail's OBJECTINFO::kill_velocity before restoring a remembered contact plane in CTransition::validate_transition @ 0x0050AA70, retaining full downhill velocity while repeatedly re-grounding the mover. The exact non-OK-only restore/kill order and final last-known validity overwrite are now ported, focused/full gates pass, and the user accepted repeated slope jumps. Evidence: docs/research/2026-07-31-269-slope-stop-capture.md.
  • #271 closed 2026-07-31 — a bounded stair-side trace proved ACDream could bypass retail's current-position edge back-probe by promoting a stale LastWalkable tread. That made PrecipiceSlide reverse an uphill tangent and rapidly carry the player down the stairs. The two stale-history substitutions are removed; current-walkable, back-probe, and no-walkable outcomes now follow CTransition::edge_slide @ 0x0050B3D0. The exact captured frame is pinned in the existing installed-stair fixture and the complete Release suite passes 10,062 tests / 5 skips. The user accepted repeated uphill runs while pressing into the stair sides. Evidence: docs/research/2026-07-31-271-stair-side-slide-capture.md.
  • #272 complete and user-accepted 2026-07-31CACQualities::InqLoad consumes enchantment-adjusted Strength through InqAttribute, but Runtime movement and both retained burden displays read raw Strength and did not share the enchantment invalidation edge. They now consume GetEffectiveAttribute(Strength) and Spellbook.EnchantmentsChanged, so buff, dispel, expiration, and death purge recompute the same burden state immediately. Focused and full Runtime/App tests pass.

Matrix rows accepted so far: speed parity, roof slide, downhill bounce, flat pop, uphill landing, and #269's slope-stop feel (rows 3/4/5-partial/12-partial). The 2026-07-31 final session accepted burdened movement, exhausted jumping, wall/corner response, crowded-monster movement, two-client remote/door/portal behavior, and shallow water. The user waived the general sweep, deferred restricted-house validation as #274, and retained the separate tight-gap clearance mismatch as #273. Automated scenario 11 remains 20/20 passing. The #269 checkpoint passes 4,107 Core tests / 2 skips and 439 Runtime tests / 0 skips; the complete Release suite passes 10,061 tests / 5 skips / 0 failures.