AP-83/AP-91 claimed no current mover sets PerfectClip. The containment proof found the opposite and the contract's honest-fallback fired: the camera probe (the sole production setter) reaches BOTH ACE-derived tails live — the viewer exemption is creature-only, the shadow-list walk is unconditional, and static scenery with authored primitives is a real non-creature population. Every reach is now recorded (camera-live silently; any non-viewer mover loudly, one-shot), so a future flag change cannot exercise unreviewed ACE-derived math silently. Four tests drive the camera's exact call shape both ways; the sabotage was intelligently adapted — there was no existing cut to disable, so it flips the one axis the proof depends on (IsCreature) and asserts reachability inverts. Both register rows rewritten CONTAINED-not-dormant with severity narrowed to camera-feel (the probe never commits a PhysicsBody). Landing note: diagnostics-only diff (two guard calls + counters + corrected stale comments), verified directly by the session lead rather than a review cycle — the review budget went where behaviour changed tonight. Campaign S CLOSES with this landing: S1A/S1B/S2/S4/S5/S6 done, S3 cancelled, three user-passed gates, one honestly-open item — AD-66's reland, twice self-refused by its own stability gate, blocked on the #341 codegen-shape measurement instability whose ABA evidence and first discriminating experiment are filed. Clean-room suite: 11,257 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
Campaign S — collision shape & response fidelity
Opened: 2026-08-06, immediately after #333/#337 closed (ea83b043).
Status: IN FLIGHT — overnight session 2026-08-07 ledger:
S1A (AP-157) closed by measurement, no code · S1B contract ready, not
implemented · S2 contract ready, not implemented · S3 CANCELLED (planned on a
misreading — see its section) · S1B LANDED (b3e43d22, #335 closed) and S2 LANDED (9671af02, AP-155
narrowed), Session-B dungeon gate USER-PASSED 2026-08-07 evening ("Feels
good!") · S4 half-landed (AD-65 shipped and USER-PASSED at the 2026-08-07 morning gate; AD-66 withheld
behind #341's measurement anomaly; AD-69 filed) · S5 closed (fix predated the
campaign; zombie register row) · S6 LANDED (containment: the camera provably reaches BOTH ACE-derived TOI
tails live — the rows' dormancy premise was false; guarded with counters +
one-shot unverified-mover log, four tests, sabotage on the real exemption
axis; AP-83/AP-91 rewritten CONTAINED-not-dormant, severity camera-feel
only) · CAMPAIGN CLOSED 2026-08-07 night with ONE honestly-open item:
AD-66's reland is blocked by the #341 codegen-shape measurement instability
(twice self-refused by its own stability gate; the ABA evidence and the
first discriminating experiment are in #341). The user's final slope look
travels with that reland. Next: #344, #343, #341's boundary hunt, then
vendors (M4) · #330 hoist landed, wiring
withheld with a seven-point scope map · #32/#338 pre-work both closed.
Scope: the twelve remaining collision-domain items — five shape/membership
divergences, three resolution-math divergences, two undecodable-math rows, and
three open bugs.
SSOT while active: this file. Digest:
claude-memory/project_physics_collision_digest.md.
Why a campaign and not twelve tickets
Three of these rows edit the same two functions. AP-157 and the AP-156
residual both live in ShadowObjectRegistry.BuildFloodSpheres; AP-159/#335
lives one call away in CellTransit.BuildShadowCellSetFromParts and in
ShadowObjectRegistry.BuildBspPartSpheres. Shipping them as separate tickets
means three review cycles over the same code and three chances to reintroduce
each other's bugs. This project already has a written rule about exactly that
shape of work: shared-file slices are ONE agent against a pinned contract
(feedback_dont_parallelize_coupled_plan_slices).
The second reason is ordering. Membership gaps mask query gaps. We just watched it happen: AP-156 put geometry into the right cell and AP-158 threw it away one layer down, so AP-156's entire visible benefit was invisible until #333 landed. Anything upstream of the query has to be correct before a gate on the downstream math means anything.
The governing lesson from the last campaign
The register rows are leads, not specifications. Measure before you fix. The evidence, all from the last two weeks:
- AP-155(b) recorded the flood approximation as OVER-inclusive, and used that direction as the reason it was safe to defer. Measured, it was UNDER-inclusive for 428 of 530 Setups — the opposite, and the dangerous direction.
- AP-156's risk column was wrong, and its wrongness is precisely why #334 — a user-visible loss of collision — sat inside it unnoticed.
- AP-22 described an unreachable branch. 0 of 5,935 installed Setups could satisfy its guard. The correct fix was deletion, not a port.
- #331's headline claim was refuted outright. The behaviour was already retail-faithful.
So one row in four, in this exact domain, was materially wrong about its own population, direction, or existence. Every slice below opens with a measurement that can cancel it. A slice that measures its population at zero closes as "row deleted", and that is a success, not a wasted slice.
Slice order
Pre-work — two cheap unblocks, before the campaign proper
Both are the user's own outstanding reports and both are blocked on a measurement that costs far less than the fix. Neither is a campaign slice.
PW-1 — #338, the step heights. Setup 0x02000001 authors
StepUpHeight = 0.600 / StepDownHeight = 1.500; the client resolves with
0.400 / 0.400. First question is whether retail reads the authored field
at all — grep named-retail for the step-height getters and their callers.
If retail substitutes its own constants, 0.4 is correct and #338 closes as a
non-defect. ~30 minutes. Do not touch code before that answer.
PW-2 — #32, local-player cliff edge-slide. This is the other half of the
original two-bug report and the thing the user will feel most, so it does not
sit behind six slices. Research is already done (38db9fff):
CollisionInfo.SetContactPlane latches last-known at all 13 call sites where
retail's COLLISIONINFO::set_contact_plane @0x00509d80 — 22 bytes — never
does. Fix is ~20–25 lines, mostly deletion, in 2 files. Blocked on a live
ACDREAM_DUMP_EDGE_SLIDE=1 capture: the report's six-row decision table has
three rows that redirect the fix entirely. Needs the user at the client.
S1 — The flood / membership pipeline
Rows: AP-157, AP-156 residual, AP-159 / #335.
Files: ShadowObjectRegistry.cs (BuildFloodSpheres, BuildBspPartSpheres),
CellTransit.cs (BuildShadowCellSetFromParts indoor arm),
ShadowShapeBuilder.cs.
One agent. Pinned contract. Not parallelised.
This is the walk-through direction and the largest single win in the list.
- AP-159 / #335 — indoors we admit an EnvCell neighbour on a sphere test
where retail hands the part array to each cell's own
find_transit_cellsand tests every part's sphere against that cell's portal planes in cell-local space. Port the part-array overload. This is the last of AP-156's traversal residual; the outdoor half already closed with #334. - AP-157 — retail's third
calc_cross_cellsbranch floods from ONECPartArray::GetSortingSphere; we flood from every Sphere shape. Our cylinder flood also ignoresCylHeight. - AP-156 residual — we scale the flood sphere by entity/part scale; retail's
CEnvCell::find_transit_cellsreads onlyCPhysicsPart::posand nevergfxobj_scale. Note the asymmetry before changing anything: retail's cross-cell walk is itself under-inclusive for scaled parts and ours is not, so "match retail" here means deliberately adopting a retail bug. That is a decision to make explicitly with the user, not silently — over-inclusive is safe, under-inclusive is the walk-through direction.
Opens with: an installed-DAT sweep giving each row its true population and direction, measured against a DAT field that is not the one being fixed (the non-circular-oracle rule that caught AP-156's identically-zero assertion).
Gate: offline differential over installed DATs, plus one live indoor run — a dungeon with tight rooms and a door.
S2 — Static publication shape fidelity
Row: AP-155. Files: LandblockPhysicsPublisher.cs,
LandblockPhysicsContentBuilder.cs.
The static-load paths emit an authored Setup Sphere as a height-capped Cylinder. Different files from S1, so it is separable — but it must land after S1, because S1's flood consumes what these paths produce, and measuring S2's effect while S1 is in flight would confound both.
Gate: shares S1's live indoor run if S1 and S2 land together; otherwise offline only, since a shape substitution's population is fully measurable from the DATs.
S3 — Animated collision pose — CANCELLED 2026-08-07 (the slice was planned on a misreading)
Row: AP-84 — which stays exactly as it is.
This plan's original S3 text claimed "a door's collision stays where the shut door was." That scenario cannot occur, and AP-84's own row says why: an open door is ETHEREAL (#150) and bypasses collision entirely, so the only pose a door ever collides in IS the registered default pose. The row's risk column already carries the honest residual — "an entity whose server-driven motion state materially moves a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case)" — with the revisit trigger written. The register was right; this plan's summary of it was wrong, which is the same reading failure the campaign's own governing lesson warns about, committed by the campaign plan itself.
No fix, no gate, no door row in the morning sitting. AP-84 remains an active, deliberate approximation.
S4 — Push-out math
Rows: AD-65 + AD-66. File: TransitionTypes.cs (AdjustOffset) — both
in the same function, so one slice.
- AD-65 — the
collisionAngle > 0arm substitutesresult -= N * anglefor retail'sPlane::snap_to_plane. Recorded effect: downhill XY travel short by cos²θ (25% at 30°, 50% at 45°). - AD-66 — the safety push-out substitutes
radius * ContactPlane.Normal.Zfor retail's bareradius, in both the trigger comparison and thezDistnumerator.
Correction carried in from the closeout: AD-65 was previously described as
"a live lead for #269". That framing is retracted — #269 closed 2026-07-31
on the user's own live gate, and AD-65's sign is opposite to that symptom.
AD-65 stands on its own merits. (#269's do-not-retry covers friction and jump
chains, which are byte-exonerated; AdjustOffset is a different function and
is not covered by it.)
This is feel, not pass-through. It cannot be gated by a test asserting "did I fall through" — it needs a movement-feel gate.
S5 — The Sledding flatness constant
Row: AD-55. File: PhysicsBody.cs (calc_friction).
We compare GroundNormal.Z > 0.99999536f (≈0.175° from flat); the raw decomp
literally computes __fcos(0.17453292519943295) = cos(10°) ≈ 0.984808. One of
the two is a decode artefact. Resolve by byte-decoding the constant from the
PDB-paired binary — there is a documented method for exactly this
(reference_pe_byte_decode), and it has already caught one inverted mapping
this project inherited from ACE.
Cheap. Batch its live gate with S4's — both are movement feel on slopes, and asking for two separate slope-feel sessions wastes the only genuinely scarce resource in this campaign.
S6 — Containment, NOT a fix
Rows: AP-83, AP-91.
These are not portable and should not be listed as fixes. The PerfectClip
time-of-impact tails in CCylSphere::collide_with_point and
CSphere::collide_with_point are x87 sequences that do not decompile legibly;
we took them from ACE. There is no retail text to port. Pretending otherwise
would put a "port it" ticket in the backlog forever.
The honest deliverable is containment: prove no production mover sets PerfectClip, and add a guard or test that fails loudly if one ever does. Then the rows describe a branch we can show is unreachable, which is the same resolution AP-22 got.
Parallel track — #330, headless live-entity collision
The headless host registers no live-entity collision at all: a bot walks through every NPC and every server-spawned object. The graphical client is unaffected.
Genuinely parallel. Different host, and — unlike everything above — it needs no live gate from the user at all, because the headless suite can assert it directly. It is the one item that can proceed while the user is unavailable, which makes it the right thing to pick up whenever a live gate is blocking.
Gate economics
The user's time at the client is the only scarce resource here. Live gates are therefore batched, not per-slice:
| Session | Covers | What to do in-world |
|---|---|---|
| A | PW-2 capture | drive the cliff edges that misbehave; capture only, no fix yet |
| B | S1 + S2 | a tight dungeon with a door; walk boundaries, jump, drop a corpse |
| C | S3 | doors: open, walk through, close, walk into |
| D | S4 + S5 | slopes: run down, run across, sled, land on inclines |
Four sessions for the whole campaign. Everything else is offline.
Definition of done
- Each row either retired with its evidence, or rewritten with a corrected population/direction, or deleted as describing something that does not exist. All three are acceptable outcomes.
- No row is closed on a test that re-computes the production expression as its own oracle. Use an independent DAT field or an independent implementation.
- Every discriminating test is sabotage-verified: break the production line and watch the test redden, in the same session it is written.
docs/ISSUES.mdand the divergence register updated in the same commit as the code, per the register's own two binding rules.