acdream/docs/research/2026-08-07-ap159-s1b-contract.md
Erik 04b794ad7c docs: overnight contracts — #330 headless collision, AP-159 S1B box-admit; morning-gate skeleton
Campaign S night shift. #330's contract pins the scoping facts so the
implementer inherits measurements instead of re-deriving them: the
builder is already presentation-free logic (the only App coupling is one
identity-guard parameter), headless has full content (_content.Dats +
prepared PhysicsDataCache), shadow-sync already runs in Runtime once a
shadow exists, and the no-window inbound route is host-disjoint per
AD-64 so registration wired there cannot double-register on the
graphical host. The local-player ProvenShapeless pin is explicitly OUT —
fixing it blind risks the K-series gates.

AP-159's S1B contract maps the pseudo-C line ranges for the part-array
find_transit_cells overload, the box-vs-cell BSP traversal, and flags
the adjacent overload's Binary Ninja signature artifact for the
mandatory pseudocode step to resolve. House rule carried: a new
traversal in two representations ships with an exact differential
referee, and the direction assertion (membership strictly shrinks) is a
test, not an assumption.

Also settled at scoping, evidence in the S1A brief: AP-157's CylHeight
half is a NON-divergence — CObjCell::find_cell_list's cylsphere overload
@0x0052b9f0 copies localtoglobal(low_pt) + radius per cylsphere, capped
at 10, and never reads height; acdream's base-point cylinder flood is
exactly retail's behaviour. Register correction follows with S1A's
measurement numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:54:13 +02:00

6.3 KiB
Raw Blame History

AP-159 / #335 contract (Campaign S slice S1B) — port the INDOOR box-admit half of retail's part-array find_transit_cells

Date: 2026-08-07 (overnight). Scoped by: the session lead. Implementer: one Sonnet agent against THIS contract. Review: dual Opus after. Severity honest-check: LOW — over-inclusive only (extra indoor broadphase candidates, never a missed one). It is in scope tonight because it is the last half of AP-156's traversal residual and Campaign S's S1; if any step below turns out larger than described, STOP and report rather than compressing quality.

The divergence

acdream's indoor cell-membership walk for multi-part BSP objects (CellTransit.BuildShadowCellSetFromParts, indoor arm) admits a neighbouring EnvCell on a sphere-vs-portal-plane test (FindTransitCellsSphere over BuildBspPartSpheres output). Retail's part-array CEnvCell::find_transit_cells uses the sphere only as a cheap reject; the ADMITTING test is box-vs-plane, followed by a box-vs-cell BSP gate in the destination cell. Full disassembly-backed spec: the #335 entry in docs/ISSUES.md (steps 14 with addresses) — that entry is the oracle of record; this contract adds the pseudo-C line map.

Oracle map (grep-named-first is already done — these are the exact places)

docs/research/named-retail/acclient_2013_pseudo_c.txt:

  • Part-array overload CEnvCell::find_transit_cells(this, count, CPhysicsPart** parts, CELLARRAY*) @0x0052cae0 — lines 310127310257. Per portal x per part: sphere cheap-reject (physics_sphere centre through Position::localtolocal, eps = F_EPSILON + radius @0x0052cb65) -> box admit (CPhysicsPart::GetBoundingBox @0x0050d600 -> BBox::LocalToLocal @0x005b1e60 -> Plane::intersect_box @0x005aa170 @0x0052cc05) -> if portal-side differs: other_cell_id == 0xFFFFFFFF sets leads-outside, else CCellPortal::GetOtherCell (threading do_not_load_cells) then box-vs-destination-BSP CCellStruct::box_intersects_cell @0x00533910 (line 317675: one-liner into BSPTREE::box_intersects_cell_bsp @0x005398b0 -> BSPNODE::box_intersects_cell_bsp @0x0053c880, body at line 325993) gating the add @0x0052cc5a; after all portals, leads-outside runs add_all_outside_cells @0x0052ccea.
  • Adjacent overload @0x0052c820 (lines 309968310126) also contains a box refinement block (@0x0052c76c0x0052c7d2). Its Binary Ninja signature (CSphere const* arg4) is inconsistent with its body indexing parts — a known BN artifact class. Your MANDATORY pseudocode step (below) disentangles the two overloads BEFORE any C# is written; if your reading contradicts the #335 entry's step list, STOP and report.
  • Plane::intersect_box @0x005aa170 — line 439037. Note its result contract (the caller accepts == 3 || == side).
  • BSPNODE::box_intersects_cell_bsp @0x0053c880 — line 325993. This is the box-vs-cell traversal you must port TWICE (graph + flat, below).

Deliverables, in order

D0 — pseudocode doc FIRST (docs/research/2026-08-07-ap159-pseudocode.md): the part-array overload and the box-vs-BSP traversal, translated to readable pseudocode with the addresses inline, per the repo's mandatory workflow. The overload-signature confusion above must be resolved here. No C# before this file exists.

D1 — the box primitives, in BOTH representations + referee. BSPQuery (graph) and FlatBspQuery (production flat) each gain the box-vs-cell-BSP traversal (BoxIntersectsCellBsp), and Plane-vs-box gains intersect_box semantics wherever the shared math lives. House rule from Slices I4/I5: a NEW traversal in two representations ships with an exact differential referee test — same inputs through both, assert identical verdicts over (a) synthetic cells covering each BSP node type and (b) a sweep of installed-DAT EnvCells (follow FlatBspQueryDifferentialTests patterns).

D2 — rewire the indoor arm. BuildShadowCellSetFromParts's indoor arm follows retail's order exactly: per portal x per part sphere cheap-reject -> box admit -> other-cell resolution with do_not_load_cells -> destination box_intersects_cell gate -> leads-outside flag -> AddAllOutsideCellsFromParts after the portal loop. The box data ALREADY EXISTS: ShadowPartBox (worldParts) is passed in today and used only by the outdoor rectangle. Do NOT touch the outdoor arm, the AD-49 seed-time rectangle, or the static prune. The outdoor building bridge (CheckBuildingTransit, retail check_building_transit @0x0052c5d0) has the same sphere-as-admit defect: port it ONLY if it reuses D1's primitive with a small diff; otherwise leave it and report it as the explicit remainder.

D3 — conformance + direction tests.

  1. A synthetic two-EnvCell fixture where a part's SPHERE overlaps the portal plane but its BOX does not: pre-fix the neighbour is admitted, post-fix it is not. Sabotage-verify: re-widen the admit to the sphere test and the test must redden.
  2. The inverse guard: a part whose box DOES cross admits — unchanged.
  3. Direction assertion over an installed-DAT sweep: the post-fix indoor membership set for every swept object is a SUBSET of the pre-fix set (over-inclusion strictly shrinks; any ADDED cell is a defect). Report counts: objects swept, cells removed, cells added (must be 0).

D4 — measurement in the report: from D3.3, the installed-DAT population whose membership actually changed, with three worst examples (object, cells before/after). If the population is ZERO, say so plainly — that is a valid outcome and the register row then records "ported, no installed data affected".

Scope — OUT

  • The AP-156 SCALE residual (retail doesn't scale flood spheres; we do) — reserved for the user's explicit decision. Do not change scaling anywhere.
  • The outdoor arm, AddAllOutsideCellsFromParts, the 3x3 sphere path, anything in ShadowObjectRegistry.
  • Register/ISSUES edits — the session lead does those.

Acceptance

Build green; FULL suite dotnet test AcDream.slnx -c Release -m:1 green with exact totals reported; D3.1 sabotage performed and reported verbatim; nothing committed. Work ONLY in C:\Users\erikn\source\repos\acdream with absolute paths; do NOT spawn subagents. If any oracle reading contradicts the #335 entry or this contract, STOP and report the contradiction.