acdream/docs/research/2026-08-06-337-neftet-wedge-mechanism.md
Erik 3171f43002 docs: file #338 — player steps at 0.400 where Setup 0x02000001 authors 0.600/1.500
Spotted in the #337 [support] capture and deliberately left out of that
fix so the fix stayed falsifiable. Filed with what is NOT established
attached: whether retail reads the authored Setup field at all is the
first question, and the entry says to grep named-retail before touching
anything. The #337 lineage already burned two diagnoses reasoned from
source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:17:28 +02:00

279 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #337 — the Neftet plateau wedge: mechanism, measured
**Date:** 2026-08-06
**Status:** mechanism proven offline; **fix LANDED 2026-08-06 — the preferred
option below was taken, the filter is deleted.** Awaiting the user's live
acceptance at the Neftet plateau.
**Reproducer:** `tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs`
**Related:** #333 (filed: the broadphase reach filter has AP-156's defect at
the query site), #334 (`13fcf381`, registration extent walk), AP-156
(`b52967de`, flood sphere placement).
---
## Verdict in one paragraph
The collision mesh is present, correctly shaped, correctly placed in the
world, and the BSP traversal reaches every part of it. **The mover never gets
as far as the query.** `Transition.FindObjCollisionsInCell`'s per-object
broadphase measures the mover's distance to the shadow entry's `Position`
the part **origin** — and compares it against `obj.Radius`, which is the
physics-BSP **root bounding sphere's radius**. For this rock those two points
are 23.6 m apart, so a mover standing on the plateau is inside the real
bounding sphere by ~20 m of margin and is still rejected. Retail has no such
filter at all. Everything else in the symptom set — the wedge, the sink, the
corpse fall-through, ACE's spawn refusal, the 16,278 m/s rejection, the
character vanishing from a retail observer's view — is downstream of that one
rejection.
---
## What was refuted, and by what measurement
### 1. "The rock's own mesh never collides" — TRUE for `0xC8766002`, and it is INNOCENT
`0xC8766002` / `gfx=0x010046DE` really does report 11,014 `tested-ok` with
zero `tested-adjusted`, `tested-collided` or `tested-slid` across the whole
capture. That is correct behaviour: **its geometry is 22.8 m away from the
wedge point.** Over a 30 × 30 × 12 m lattice covering the whole plateau
(12,493 points) it has **zero** brute-force hits — it simply is not there.
It is a candidate in that cell only because it is a 130 × 147 m owner whose
registration legitimately spans the cell. The probe families that reach it are
working exactly as designed. This object was a red herring.
The neighbours do collide: `0xC8766003` 767 adjusted / 26 collided,
`0xC8766009` 1,135 adjusted / 708 slid / 58 collided. The rock the player
actually walks on is **`0xC8766009` / `gfx=0x01004751`**.
### 2. Wrong world transform — REFUTED
The offline reconstruction from the installed DAT reproduces the runtime
placement exactly: `0xC8766002` at `(84.699, 100.082, 13.000)` yaw 45.00°
against the live `[geom]` line's `objPos=(84.70,100.08,13.00)` and its
`bspCentreOffset` (which implies 45.00°). All eleven owners match. The
transform is right.
### 3. BSP traversal hole / bounding-sphere early-out — REFUTED
A referee ran the production walk (`FlatBspQuery.SphereIntersectsPoly`,
node bounding-sphere early-outs and all) against brute force over every
polygon the tree indexes, at 7,770 probe points placed on both faces of every
polygon of all eleven owners, plus 137,423 lattice points over the plateau.
**Mismatch = 0 everywhere.** There is no pocket the traversal cannot reach.
### 4. Missing / degenerate geometry, a hole in the walking surface — REFUTED
A 0.5 m hole map over the plateau shows continuous upward-facing physics
coverage across the whole wedge region; the only gaps are outside the rock's
footprint. At the wedge XY the column is closed: an up-facing polygon at
z = 50.985 (nz = +0.9805) over a down-facing one at z = 40.597 (nz = 0.9325),
i.e. 10.4 m of solid rock, with the player's feet at z = 50.011 — **0.975 m
inside it.**
### 5. `[geom] verdict=coincident` — confirmed to mean less than it looks
`LogGeometry` compares `physicsMin/Max` against `visualMin/Max`, both taken
from the same GfxObj asset in the object's **own local frame**. No world
transform enters the comparison. `coincident` proves shape agreement only.
It happens to be true here, but it could never have been the discriminator.
---
## The mechanism, measured
### The frame-by-frame (337-support.log, cell `0x8766002B`)
Landblock-local coordinates; the log's Y carries a +576 m live-centre offset,
removed here. Player feet position; Setup `0x02000001` gives sphere[0] at
feet + 0.475 with r = 0.480 and sphere[1] at feet + 1.350.
| t | feet z | surface z at that XY | what happened |
|---|---|---|---|
| …218906219296 | 51.096 | 51.081 | standing correctly (delta +0.015). **Every horizontal move of 0.250.53 m returns `moved=0.000, stalled=True`** on an 11° walkable slope. This is the wedge. |
| 219328 | 51.096 → 51.389 | | player jumps to escape |
| …219937 | → 54.638 | | free rise, `support=none` |
| …221125 | 54.638 → 50.021 | 51.183 | **falls straight through the surface.** Six consecutive resolves accept the full commanded step with no contact plane. |
| 221156 | 50.021 | | descent finally blocked, still no contact plane |
| 221671 | 49.908 | 51.127 | `cpSrc=ValidateTransition:6076` — retail's stationary-fall failsafe **manufactures** a flat plane at z = 49.903, 1.2 m below the real rock surface |
The `support=object cpNz=1.0000` readings inside the rock are not the rock.
`ValidateTransition:6076` is retail's `FramesStationaryFall > 1` synthetic
up-plane through the sphere bottom; `ValidateTransition:5997` is retail's
`LastKnownContactPlane` restore, i.e. a **stale** plane retained from frames
when the object was still admitted. Both are retail-correct responses to a
stuck body — which is why the client believes it is standing while ACE
believes the position is invalid.
### The replay: the query would have hit
`Issue337NeftetRockGeometryInspectionTests.ReplayTheDescentThatFellThrough…`
runs each of those six descent steps as a swept sphere against `0xC8766009`'s
real BSP:
```
CONTROL climb step ent=0xC8766009 sphere[0] swept=True(poly 22) static=True nearest=0.070 m
CONTROL stand step ent=0xC8766009 sphere[0] swept=True(poly 26) static=True nearest=0.003 m
FALL 2 51.269->50.961 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.248 m
FALL 3 50.961->50.670 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.037 m
FALL 4 50.670->50.335 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.366 m
```
The geometry is right there and the production primitive returns the hit.
Path 6 of `FlatBspQuery.FindCollisionsCore` (the airborne dispatch) would have
called `path.SetCollide(...)` and returned `Adjusted`, blocking the fall.
**It was never called.**
### Why it was never called
`Transition.FindObjCollisionsInCell` (`src/AcDream.Core/Physics/TransitionTypes.cs`,
the `maxReach` test in the candidate loop):
```csharp
Vector3 deltaToCurr = currPos - obj.Position; // ← part ORIGIN
...
float maxReach = sphereRadius + obj.Radius // ← BSP ROOT SPHERE radius
+ movement.Length() + 2f;
if (distToCurr > maxReach) continue;
```
For `0xC8766009`: origin `(159.107, 36.629, 0.005)`, root bounding sphere
centred at local `(1.753, 14.259, 18.667)` with radius 56.909 — i.e. the
sphere centre sits **23.556 m** from the origin the filter measures to.
At the fall position, measured (reproducer output):
```
distToOrigin = 60.434 m > maxReach = 59.697 m → REJECTED
distance to the BSP bounding-sphere CENTRE = 37.083 m ≪ 56.909 m radius
```
The live capture recorded the same thing 7,225 times for this owner, and the
probe's own `wouldAcceptAtCenter` column says `True` on **every** rejection:
```
currPos=(126.57,36.59,50.71) distOrigin=60.24 budget=59.64 shortfall=+0.60 acceptAtCentre=True move=0.255
currPos=(126.57,36.59,50.71) distOrigin=60.24 budget=60.10 shortfall=+0.14 acceptAtCentre=True move=0.715
```
### Why it is bounded, and why jumping over it works
The dead zone is the shell between `distOrigin = maxReach` and the true
bounding sphere. Because the sphere centre is offset 23.556 m from the origin,
that shell is up to ~23.5 m thick on the far side of the object — here it
covers the top of the plateau and nothing else. Everywhere closer to the
origin, the same mesh collides normally. That is the "fails in a bounded
region, works elsewhere on the same object" property.
`movement.Length()` is a term in the budget. A walking step of 0.25 m gives
shortfall +0.60 (rejected); a step of 0.72 m gives +0.14; a step of ~0.86 m
passes. **A jump's larger per-frame movement inflates the budget and lets the
object back through the filter.** That is why jumping over the spot works and
walking into it does not, and why a corpse — small per-frame movement — falls
straight through.
---
## Retail
There is no per-object distance filter on retail's BSP branch. Verified
instruction-by-instruction with cdb against the PDB-paired v11.4186 binary
(`C:\Users\erikn\Downloads\acclient.exe`, `check_exe_pdb.py``MATCH`,
GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`):
- `acclient!CPartArray::FindObjCollisions` @ **0x00518180** — 14 instructions:
a bare `do/while` over `parts[i]` calling `CPhysicsPart::find_obj_collisions`
and breaking on `!= OK`. No compare, no float math.
- `acclient!CPhysicsPart::find_obj_collisions` @ **0x0050d8d0** — 17
instructions: null-check `gfxobj`, null-check `gfxobj->physics_bsp`
(`[ecx+78h]`), `SPHEREPATH::cache_localspace_sphere`,
`CGfxObj::find_obj_collisions` @ 0x00534700. No compare, no float math.
`CPhysicsObj::FindObjCollisions` @ 0x0050f050 reaches that pair through
`CPartArray::FindObjCollisions` at 0x0050f192; its cylsphere/sphere loops
(0x0050f1c4 / 0x0050f251) call the real `intersects_sphere` per primitive —
they are tests, not pre-filters. Retail's only spatial rejection is the BSP
node bounding-sphere test inside the walk, which is correctly centred.
The in-tree comment claiming the filter is "the analog of the part
sorting-sphere early-outs inside retail's `CPhysicsObj::FindObjCollisions`
response-neutral, pure perf" is **wrong on both counts.**
---
## The fix, as landed
**Taken: the preferred option — the per-object distance pre-check is DELETED**,
for BSP and primitive entries alike. Retail has none, and the BSP walk's own
root-node bounding-sphere test is the correctly-centred early-out that makes a
second one unnecessary. The comment that called the filter "the analog of the
part sorting-sphere early-outs inside retail's `CPhysicsObj::FindObjCollisions`
— response-neutral, pure perf" was false in both halves and is replaced by the
disassembly that refutes it. **AP-158 is retired**; no new register row is
created, because the code no longer diverges.
The fallback — measuring to the bounding-sphere centre, which would have needed
`BoundsCenter` on `ShadowEntry` and both registration paths — was NOT taken. It
would have kept a construct retail does not have, including a `+ 2f` slack and
a `movement.Length()` term with no retail counterpart, and left a second reach
budget to be tuned forever.
### Gates
`tests/AcDream.Core.Tests/Physics/Issue333BroadphaseReachFilterTests.cs` drives
the production path end-to-end (`ResolveWithTransition`
`FindObjCollisionsInCell``CollisionTraversal`) on a DAT-free fixture, so it
runs everywhere rather than only where the installed DATs are present. It is a
discriminating PAIR, sabotage-verified: with the `maxReach` pre-check restored,
`OffCentreBspFloorStopsAFallingMover` fails — the mover reaches z=37.800, which
is exactly the unobstructed fall, with `blockedAtLeastOnce=False` — while
`CentredBspFloorStopsAFallingMover` keeps passing. Without the control row, a
fixture that simply could not fall would pass the first test for the wrong
reason.
The installed-DAT evidence for THIS rock is
`Issue337NeftetRockGeometryInspectionTests.TheOldBroadphaseMeasuredToTheOriginAndSoRejectedGeometryItStoodOn`
(previously the skipped `TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn`,
which asserted the now-deleted predicate and could never have gone green). It
pins both halves of the diagnosis: the origin-measured distance OUTSIDE the old
budget, and the centre-measured distance comfortably INSIDE the same radius. If
a future DAT or transform change makes either false, the mechanism recorded here
no longer describes this object.
### Perf — measured, not assumed
Deleting a filter costs whatever the candidates it used to reject now cost.
Measured in Release on a synthetic all-BSP cell, per `ResolveWithTransition`:
| candidates in cell | with filter | without | delta |
|---|---|---|---|
| 38 — the live maximum | 10.61 µs | 16.68 µs | +6.07 µs (1.57×) |
| 200 — 5× anything observed | 17.34 µs | 39.48 µs | +22.1 µs (2.28×) |
≈ 0.16 µs per additional candidate actually tested; the curve is linear, and
the 200-object row is included only to show that, not to suggest it is
reachable. The live population is the bound that matters: over **19,701**
`[reach-q]` samples across the Neftet and outdoor captures (`334-fix-gate.log`,
`334-neftet-probe.log`, `334-neftet.log`) the in-cell candidate count is
**p50 = 9, p99 = 32, max 38**. Retail pays the same cost and shipped without a
filter.
### Probe columns kept deliberately
`rejectedReach` on the `[reach-q]` line and the origin-vs-centre distance pair
on `[reach-obj]` are RETAINED and are now structurally zero / purely
informational. That is the point: a post-fix capture reading `rejectedReach=0`
is directly comparable with the pre-fix capture that recorded **7,225**
rejections on a single owner, every one of them with `wouldAcceptAtCenter=True`.
Dropping the columns would make the two captures incomparable.
---
## Separate observation, not part of this defect — now filed as #338
Setup `0x02000001` authors `StepUpHeight = 0.600` and
`StepDownHeight = 1.500`. The live `[support]` lines show the player resolving
with `stepUp=0.400 stepDown=0.400`. A 1.5 m step-down is what keeps a mover
attached to a descending slope; 0.4 m is not. Worth its own investigation —
it does not cause this wedge, and it was not chased here.