Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
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>
266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# 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_cells` and
|
||
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_cells` branch floods from ONE
|
||
`CPartArray::GetSortingSphere`; we flood from every Sphere shape. Our
|
||
cylinder flood also ignores `CylHeight`.
|
||
- **AP-156 residual** — we scale the flood sphere by entity/part scale; retail's
|
||
`CEnvCell::find_transit_cells` reads only `CPhysicsPart::pos` and never
|
||
`gfxobj_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 > 0` arm substitutes `result -= N * angle`
|
||
for retail's `Plane::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.Z`
|
||
for retail's bare `radius`, in both the trigger comparison and the `zDist`
|
||
numerator.
|
||
|
||
**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.md` and the divergence register updated in the **same commit**
|
||
as the code, per the register's own two binding rules.
|