docs: file #333 — the broadphase reach filter has AP-156's defect at the query site

Found while fixing AP-156 and deliberately NOT bundled into it: the shadow
broadphase at TransitionTypes.cs:3756-3764 measures `currPos - obj.Position`
against `sphereRadius + obj.Radius + movement + 2f`, where `obj.Position` is
the PART ORIGIN but `obj.Radius` is the BSP root bounding sphere's radius,
measured about that sphere's own centre. The same discarded origin, one layer
down.

It admits a real contact only when the sphere's centre is within about
`movement + 2` metres of the part origin. For Setup 0x02000255 that offset is
9.911 m against a budget near 2.5 m, so a mover touching the upper half of the
prop is discarded before BSPQuery runs.

This is newly load-bearing: before AP-156 those objects were mostly not in the
cell at all, so the filter never got to reject them. AP-156 puts them in the
right cells and this becomes the next gate. It is the first place to look if
the connected session finds a tall prop that still does not block.

Filed rather than fixed because it is a different code path with an unanswered
retail question — the `+ 2f` slack and the movement term look like acdream's
own broadphase rather than a port of anything in CPhysicsObj::FindObjCollisions
@0x0050f050, in which case it needs a divergence row of its own before it is
touched. Bundling it would also make AP-156's connected gate un-attributable,
which is exactly the fault that split AP-155.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-06 15:54:35 +02:00
parent b52967def3
commit e2b2d04cb5

View file

@ -24,6 +24,77 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #333 — The shadow broadphase reach filter measures from the PART ORIGIN, so an off-centre BSP part can be in the right cell and still never be tested
**Status:** OPEN
**Severity:** high for the tall-prop population. It is the gate immediately
downstream of the AP-156 membership fix, so that fix alone may not be enough to
make the worst objects block.
**Filed:** 2026-08-06 at the AP-156 fix (commit `b52967de`), which surfaced it.
**Do NOT bundle with AP-156.** Different code path (collision query, not cell
membership), and it needs its own retail question answered first.
### The mechanism
`src/AcDream.Core/Physics/TransitionTypes.cs:3756-3764`, the shadow broadphase:
```csharp
Vector3 deltaToCurr = currPos - obj.Position;
...
float maxReach = sphereRadius + obj.Radius + movement.Length() + 2f;
if (distToCurr > maxReach)
continue; // candidate discarded, never tested
```
`obj.Position` is the shadow row's PART placement —
`entityWorldPos + rotate(shape.LocalPosition, entityWorldRot)` — while
`obj.Radius` is the physics-BSP ROOT BOUNDING SPHERE radius, which is measured
about that sphere's own centre, not about the part origin. AP-156 established
that the two are frequently far apart: 376 of 973 installed physics-BSP parts
have a root-sphere origin further from the part origin than half their own
radius, worst 20.762 m on a 27.708 m sphere.
Write `d` for the distance from part origin to the true sphere centre, `R` for
`obj.Radius`, `r` for the mover's sphere radius. A mover just touching the
geometry is `R + r` from the sphere's TRUE centre, hence up to `d + R + r` from
`obj.Position`. The filter admits it only when
`d + R + r <= r + R + movement + 2`, i.e. only when `d <= movement + 2`. Per
physics tick the movement term is well under a metre, so any part whose root
sphere sits more than about 2 m from its part origin can have a genuine contact
discarded before `BSPQuery` ever runs.
Worked case — Setup `0x02000255`, one part, root sphere
origin `(0.000, -0.007, 9.911)`, radius `10.522`, `Setup.Height = 18.692`.
`d = 9.911` against a budget of roughly 2.5 m. A player standing against the
upper half of that prop is about 20.4 m from the part origin while `maxReach`
is about 13.5 m. Discarded.
### Why it matters now
Before AP-156 those objects were usually not registered in the cell at all, so
this filter never got the chance to reject them. AP-156 puts them into the
correct cells; this filter is the next gate they hit. If the connected session
finds a tall prop that still does not block after AP-156, look here first.
### What to establish before fixing
1. Does retail have this pre-filter at all? `CPhysicsObj::FindObjCollisions`
@0x0050f050 walks the cell's object list and dispatches per object; the
`+ 2f` slack and the `movement.Length()` term look like acdream's own
broadphase rather than a port. If it is acdream's, it also needs a
divergence-register row, which it does not currently have.
2. If it is kept, it must measure from where the geometry is: `ShadowEntry`
needs the `BoundsCenter` that `ShadowShape` now carries, and `deltaToCurr`
must be taken against `obj.Position + rotate(obj.BoundsCenter, obj.Rotation)`.
3. Cylinder rows are unaffected (`BoundsCenter == Zero` by construction) — the
change must not move them.
### Test to write first
A mover adjacent to an off-centre BSP part's geometry, with the part origin far
outside `maxReach`. It must reach `BSPQuery`. Sabotage by restoring the
part-origin measurement; it must go back to reporting no collision.
## #332 — Headless bots appear to have no remote dead-reckoning at all ## #332 — Headless bots appear to have no remote dead-reckoning at all
**Status:** OPEN (observation, not yet established as a defect) **Status:** OPEN (observation, not yet established as a defect)