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>
14 KiB
#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):
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 baredo/whileoverparts[i]callingCPhysicsPart::find_obj_collisionsand breaking on!= OK. No compare, no float math.acclient!CPhysicsPart::find_obj_collisions@ 0x0050d8d0 — 17 instructions: null-checkgfxobj, null-checkgfxobj->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.