From e2b2d04cb523810300b80a1fb6e49d26f057390d Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 6 Aug 2026 15:54:35 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20file=20#333=20=E2=80=94=20the=20broadph?= =?UTF-8?q?ase=20reach=20filter=20has=20AP-156's=20defect=20at=20the=20que?= =?UTF-8?q?ry=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/ISSUES.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 4655bdfc..9e33e213 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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. - 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 **Status:** OPEN (observation, not yet established as a defect)