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>
279 lines
14 KiB
Markdown
279 lines
14 KiB
Markdown
# #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 |
|
||
|---|---|---|---|
|
||
| …218906–219296 | 51.096 | 51.081 | standing correctly (delta +0.015). **Every horizontal move of 0.25–0.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.
|