fix(physics): S1B — indoor cell membership admits on the part BOX, as retail does (#335, AP-159 narrowed)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

CellTransit.FindTransitCellsBox ports CEnvCell::find_transit_cells'
part-array overload @0x0052cae0 line-for-line: per-portal x per-part
order, the sphere cheap-reject at F_EPSILON+radius, the box admit whose
"Straddle or crossing-side" rule is exactly retail's `eax != side` under
the PDB Sidedness enum, leads-outside placed AFTER the admit, the
unconditional unloaded-neighbour hint without the sphere overload's
re-test, the destination box_intersects_cell gate with its deliberate
no-break, and add_all_outside_cells after the loop. The box-vs-cell BSP
traversal lands in BOTH representations behind the flat-authoritative
dispatcher with a graph referee whose 20,000 installed comparisons are
pinned by assertion (review F5), zero mismatch.

Dual Opus review: PASS on both lenses. The mandatory D0 pseudocode pass
caught that the contract's own supplementary note misattributed the box
block to the sphere overload — it belongs to a SECOND
check_building_transit overload @0x0052c680, whose portal-side
convention is INVERTED and whose admit differs; the pseudocode doc now
records that trap plus two byte confirmations made at review:
which_side @0x00444720 is strictly > eps for POSITIVE, and
intersect_box's in-plane early exit returns CROSSING(3)
(jp @0x005aa1bc -> mov eax,3), settling review items b1/b2 for the
future bridge porter. The bridge itself stays unported as AP-159's
explicit remainder.

The review also retired #335's severity premise honestly: "over-
inclusive only, never a missed one" is wrong at production shape ratios,
where the box (whole-vertex AABB) legitimately exceeds the sphere
(physics-polygon root sphere). Measured, both populations: rigged
(box << sphere) — 1,520 placements, 978 cells removed, 0 added;
production-ratio (box >= sphere) — 950 placements, 20 removed, 1 ADDED
through the loaded-neighbour gate, which is retail's direction, not a
defect. The no-op guard (review F4) asserts removal is nonzero so an
unwired admit cannot pass silently.

Process note: the implementer authored against this session's worktree
at bec5c69d, 25 commits stale — the recorded worktree-base class. All
six files were byte-identical between bases, the diff transplanted
losslessly, and every verdict-bearing run (referee, direction sweeps,
this clean-room) was re-executed on current main. S2's uncommitted
phase-1 edits were stashed for this landing so the suite verdicts
exactly one changeset.

Also untracks 341-slope-capture.jsonl (an accidental add) and
gitignores it.

Clean-room suite: 11,248 passed / 6 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-07 07:46:57 +02:00
parent 1b2580be4c
commit b3e43d22c9
14 changed files with 1600 additions and 4914 deletions

View file

@ -1106,6 +1106,115 @@ public static class BSPQuery
: true;
}
// =========================================================================
// PUBLIC: box_intersects_cell_bsp (AP-159 / #335, 2026-08-07)
// Retail: Plane::intersect_box @0x005aa170 (pc:439037), Plane::which_side
// @0x00444720 (pc:75027), BSPNODE::box_intersects_cell_bsp @0x0053c880
// (pc:325993). See docs/research/2026-08-07-ap159-pseudocode.md §2-3 for
// the full disassembly-backed derivation, including the PortalSide/raw
// portal_side mapping and the "== 3 || == side" caller contract.
// =========================================================================
/// <summary>
/// Retail <c>F_EPSILON</c> (0x007c8c70, 0.000199999995f) — the epsilon
/// <c>Plane::which_side</c> and <c>BSPNODE::box_intersects_cell_bsp</c>
/// both use for their plane-distance classification.
/// </summary>
internal const float BoxPlaneEpsilon = 0.000199999995f;
/// <summary>
/// Retail <c>Sidedness</c> (PDB-recovered enum; <c>Plane::which_side</c>
/// @0x00444720 returns these three values verbatim: 0 for clearly in
/// front of the plane, 1 for clearly behind, else the on-plane/straddle
/// case). Renamed <c>Straddle</c> here for clarity — retail's own value
/// for that third case IS pinned: acclient.h:2527 Sidedness { POSITIVE=0, NEGATIVE=1, IN_PLANE=2, CROSSING=3 }; which_side returns at most 2 (pc:75045) while intersect_box's straddle/early-exit sentinel is 3 (byte-confirmed: the in-plane early exit at 0x005aa1bc lands on mov eax,3). Collapsing both onto one Straddle is safe because every consumer tests only distinctness-from-a-pure-side
/// (masked by an early, unassigned-looking return the disassembly shows
/// is shared with <c>which_side</c>'s own straddle path).
/// </summary>
internal enum PlaneSide
{
Positive = 0,
Negative = 1,
Straddle = 2,
}
/// <summary>
/// Retail <c>Plane::which_side</c> @0x00444720 (pc:75027) — classify one
/// point against a plane with epsilon slack.
/// </summary>
internal static PlaneSide WhichSide(in Plane plane, Vector3 point, float eps)
{
float dist = Vector3.Dot(plane.Normal, point) + plane.D;
if (dist >= eps) return PlaneSide.Positive;
if (dist < -eps) return PlaneSide.Negative;
return PlaneSide.Straddle;
}
/// <summary>
/// Retail <c>Plane::intersect_box</c> @0x005aa170 (pc:439037) AND the
/// box-vs-plane test embedded in <c>BSPNODE::box_intersects_cell_bsp</c>
/// @0x0053c880 (pc:325993) — both classify a box against a plane by
/// corners in a fixed enumeration; the all-same-side result is order-
/// result is order-independent, it is an AND of per-corner agreement)
/// and returning <see cref="PlaneSide.Straddle"/> the instant any corner
/// disagrees with the first corner tested, else the shared side. Shared
/// here because both callers need identical semantics — this is the
/// "shared math" D1 of the AP-159/#335 contract asks for.
/// </summary>
// Cold-path shape note (review F15): retail classifies corner 0 first and
// short-circuits; this port materialises all 8 corners up front. Identical
// results, ~7 extra Vector3 constructions per node in the accept case —
// registration-flood only, never per-resolve.
internal static PlaneSide ClassifyBox(in Plane plane, Vector3 min, Vector3 max)
{
Span<Vector3> corners =
[
new Vector3(min.X, min.Y, min.Z),
new Vector3(max.X, max.Y, max.Z),
new Vector3(min.X, min.Y, max.Z),
new Vector3(min.X, max.Y, min.Z),
new Vector3(max.X, min.Y, min.Z),
new Vector3(max.X, min.Y, max.Z),
new Vector3(min.X, max.Y, max.Z),
new Vector3(max.X, max.Y, min.Z),
];
PlaneSide side0 = WhichSide(plane, corners[0], BoxPlaneEpsilon);
if (side0 == PlaneSide.Straddle) return PlaneSide.Straddle;
for (int i = 1; i < corners.Length; i++)
{
if (WhichSide(plane, corners[i], BoxPlaneEpsilon) != side0)
return PlaneSide.Straddle;
}
return side0;
}
/// <summary>
/// Retail <c>BSPNODE::box_intersects_cell_bsp</c> @0x0053c880
/// (pc:325993) — the box-shaped sibling of <see cref="PointInsideCellBsp"/>
/// and <see cref="SphereIntersectsCellBsp"/>: an iterative walk down
/// <c>PosNode</c> only, rejecting the instant the box is found to lie
/// entirely on the NEGATIVE side of a splitting plane (via
/// <see cref="ClassifyBox"/>), treating a null <c>PosNode</c> or a leaf
/// as "inside." Reached from <c>CCellStruct::box_intersects_cell</c>
/// @0x00533910 (pc:317675, bare tailcall) → <c>BSPTREE::box_intersects_cell_bsp</c>
/// @0x005398b0 (pc:323249, bare tailcall) → this function.
/// </summary>
public static bool BoxIntersectsCellBsp(CellBSPNode? node, Vector3 min, Vector3 max)
{
if (node is null) return true;
if (node.Type == BSPNodeType.Leaf) return true;
if (ClassifyBox(node.SplittingPlane, min, max) == PlaneSide.Negative)
return false;
return node.PosNode is not null
? BoxIntersectsCellBsp(node.PosNode, min, max)
: true;
}
// =========================================================================
// BSP TREE-LEVEL HELPERS
//