From 04b794ad7c3b9d6042c4c62e9c41240bd48e58fa Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 7 Aug 2026 00:54:13 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20overnight=20contracts=20=E2=80=94=20#33?= =?UTF-8?q?0=20headless=20collision,=20AP-159=20S1B=20box-admit;=20morning?= =?UTF-8?q?-gate=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../2026-08-07-morning-gate-checklist.md | 50 +++++++ docs/research/2026-08-07-330-contract.md | 123 ++++++++++++++++++ .../research/2026-08-07-ap159-s1b-contract.md | 111 ++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 docs/plans/2026-08-07-morning-gate-checklist.md create mode 100644 docs/research/2026-08-07-330-contract.md create mode 100644 docs/research/2026-08-07-ap159-s1b-contract.md diff --git a/docs/plans/2026-08-07-morning-gate-checklist.md b/docs/plans/2026-08-07-morning-gate-checklist.md new file mode 100644 index 00000000..d6fb8a81 --- /dev/null +++ b/docs/plans/2026-08-07-morning-gate-checklist.md @@ -0,0 +1,50 @@ +# Morning gate — 2026-08-07 (one sitting) + +**Status: DRAFT — the overnight session updates this as work lands. Do not +run it until this banner says READY.** + +Everything below folds the deferred visual gates into one sitting, ordered so +travel between sites doubles as #339 reproduction attempts. Launch is +prepared by the overnight session; **the client is NOT launched until you're +back** (your instruction). + +## 0. Launch (the lead runs this when you say go) + +Main checkout, absolute path, probes armed. The first `[step-h]` line in the +log must print the assembly path under +`C:\Users\erikn\source\repos\acdream\src\` — that is the wrong-binary guard, +now part of every capture. + +## 1. Already verified last night — nothing owed + +- **#32 local edge-slide** — you passed it at the Rithwic cliff ("Yes works + now"). Closed. +- **#338 step heights** — headline refuted by measurement; no production + change happened, so there is nothing to look at. Closed (AD-68 records the + benign residual). + +## 2. The sitting + +| # | What | Where / how | Pass looks like | +|---|---|---|---| +| G1 | *(placeholder — filled in by the overnight landings: #330 is + automated-gate-only and needs no eyes; S1/S2/S3/S4/S5 rows appear here + exactly as far as the night got)* | | | + +## 3. Free riders during travel + +- **#339 portal-space hang** — every portal you take is a reproduction + attempt; if you get stuck again, the log captures the readiness flags and + the session becomes the diagnosis session. Nothing to do actively. + +## 4. Deliberately NOT in this sitting + +- **C5c's connected-gate batch** (~1 hr: D-1 scenarios, AP-136 six-step park, + route-7, two-client, nine-stop soak) — standing debt by your direction; + say the word any morning and it becomes its own sitting. The probe family + stays in the tree until it runs. +- **AP-156 scale question** — retail does NOT scale cross-cell flood spheres; + we do. Matching retail here means adopting a retail bug whose direction is + walk-through for shrunk objects. Your call, explicitly, before any code + changes: keep our over-inclusive scaling (safe, divergent) or match retail + (faithful, under-inclusive for scale<1). One sentence from you settles it. diff --git a/docs/research/2026-08-07-330-contract.md b/docs/research/2026-08-07-330-contract.md new file mode 100644 index 00000000..871eba24 --- /dev/null +++ b/docs/research/2026-08-07-330-contract.md @@ -0,0 +1,123 @@ +# #330 contract — headless live-entity collision registration + +**Date:** 2026-08-07 (overnight session). **Scoped by:** the session lead. +**Implementer:** one Sonnet agent against THIS contract. **Review:** dual Opus +(retail-conformance + architecture) after implementation. + +## The defect + +`ShadowShapeBuilder.FromSetup` — the only producer of live-entity collision +shapes — has exactly one production caller, `AcDream.App`'s +`LiveEntityCollisionBuilder`, wired through the graphical materializer. +`AcDream.Headless` references only Runtime/Content/Core, so **no headless code +path ever builds or registers a collision shape for a server-spawned entity**. +A bot has static landblock collision but walks through every NPC, player, and +spawned object. Filed at the AP-22 blast-radius survey +(`docs/research/2026-08-06-ap22-contract.md` §5). + +## Facts established at scoping (do not re-derive) + +1. `LiveEntityCollisionBuilder` + `LiveEntityDefaultPoseResolver` are already + presentation-free logic: their imports are Core, Core.Net + (`WorldSession.EntitySpawn`), Core.World (`WorldEntity`), DatReaderWriter. + The ONLY App coupling is the `LiveEntityRecord exactRecord` identity-guard + parameter on `Build(...)` and the `AcDream.App.World` using it drags in. +2. The headless host HAS all required content: + `HeadlessSessionWorldProjection` holds `_content.Dats` (full + `DatCollection` — `Setup` and MotionTable DBObjs resolvable) and + `_content.PreparedCollision` → per-landblock `PhysicsDataCache` with flat + GfxObj physics (`GetFlatGfxObj`). +3. Once a shadow IS registered, movement follows automatically: + `RuntimeRemotePhysicsUpdater` (Runtime, both hosts) already calls + `ShouldSynchronizeShadow` at line ~844 on its tick. Registration is the + whole gap. +4. The no-window inbound entity route is + `RuntimeLiveEntitySessionController` (`src/AcDream.Runtime/Session/`), + constructed by `HeadlessSessionHost` (~line 682). Per AD-64 the graphical + host runs a PARALLEL App-side route (`LiveEntitySessionController` → + `DatLiveEntityProjectionMaterializer`, which calls + `_collisionBuilder.Build(...)` at ~line 832 and + `LiveEntityCollisionBuilder.Register` at ~line 840). Wiring registration + into the Runtime controller therefore CANNOT double-register on the + graphical host. Unification of the two routes is #324, NOT this task. +5. The graphical local-player disposition + (`SessionPlayerComposition` ~line 573) chooses + `RegisteredAuthoredPayload` vs `ProvenShapeless` by + `PhysicsEngine.ShadowObjects.HasLogicalOwner(key.LocalEntityId)`. The + headless pin (`HeadlessSessionHost` ~line 637) hardcodes + `ProvenShapeless`. + +## Scope — IN + +**A. Hoist, no behaviour change.** Move +`src/AcDream.App/Physics/LiveEntityDefaultPoseResolver.cs` and +`src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs` (with +`LiveEntityCollisionRegistration`) to `src/AcDream.Runtime/Physics/`, +namespace `AcDream.Runtime.Physics`. Change `Build(...)`'s +`LiveEntityRecord exactRecord` parameter to presentation-free primitives: +`uint expectedServerGuid, ulong expectedGeneration, WorldEntity +expectedEntity` — the guard's three comparisons keep IDENTICAL semantics +(guid match, generation match, reference-equality on the entity). Adjust +visibility so App and Headless both reach it (match how neighbouring Runtime +types are exposed to App; prefer `public` over new InternalsVisibleTo +entries). Update every App call site; `git grep LiveEntityCollisionBuilder` +first and check each hit — several are comments. + +**B. No-window registration route.** Give +`RuntimeLiveEntitySessionController` an OPTIONAL collision registrar seam +(null = today's behaviour, which the graphical host keeps). Constructed by +`HeadlessSessionHost` from `_content.Dats` + the prepared collision source + +`Runtime.EntityObjects.Physics`: +- On the controller's accepted-spawn/materialization commit (find the exact + seam by reading the controller — the same acceptance the graphical + materializer keys on), resolve the entity's `Setup` DBObj and default pose, + call the hoisted `Build(...)` with the setup's OWN part ids as + `effectivePartGfxObjIds` (headless has no appearance system — note this in + a code comment as a deliberate headless/graphical difference: an + appearance-swapped BSP part collides as the default part on headless), and + `Register` into the engine's `ShadowObjects`. +- On despawn/teardown, remove the registration the same way the graphical + path does; the K-series ownership-ledger convergence tests must stay green. +- On scale/appearance updates: headless has no appearance route; scale + arrives via the spawn — if the controller processes a scale-changing + update, re-register (check whether such a path exists; if none does, write + that down in the code comment rather than inventing one). + +**C. Tests (DAT-free, deterministic — model: +`Issue333BroadphaseReachFilterTests`).** In `tests/AcDream.Runtime.Tests/` +(or Headless.Tests if the fixture fits better — pick ONE): +1. Drive the no-window route with a synthetic accepted spawn whose Setup + resolves to a sphere shape via `PhysicsDataCache.RegisterGfxObjForTest`(or + the Setup-level equivalent the existing tests use). Assert the entity + appears in `ShadowObjects.GetObjectsInCell` for its cell, then + `ResolveWithTransition` a 0.48 m mover into it and assert the move is + blocked/adjusted — the end-to-end "bot cannot walk through an NPC" fact. +2. Teardown row: despawn removes the shadow registration. +3. SABOTAGE-VERIFY in-session: disable the registrar wiring (pass null) and + confirm test 1 reds; restore; record the sabotage result in your report. + +## Scope — OUT (do not touch) + +- The local-player `ProvenShapeless` pin (fact 5). Registering the local + player's own shadow is a separate seam (`LocalPlayerShadowSynchronizer`) + with its own lifecycle; fixing it blind tonight risks the K-series gates. + It stays pinned; the session lead files the residual. +- #324 (route unification), any graphical-host behaviour, any + divergence-register or ISSUES edit (the session lead does those). +- `ShadowObjectRegistry` / `ShadowShapeBuilder` internals. + +## Acceptance + +- `dotnet build AcDream.slnx -c Release` green. +- Full suite `dotnet test AcDream.slnx -c Release -m:1` green — report the + exact totals. +- The new tests pass, and the sabotage check was performed and reddened. +- NOTHING committed — leave the working tree for review. + +## Process rules + +Work ONLY in `C:\Users\erikn\source\repos\acdream` (absolute paths — there +are other checkouts; running or building anything by relative path from the +wrong cwd cost this project a void verdict yesterday). Do NOT spawn +subagents. If a fact above contradicts what you read in the source, STOP and +report the contradiction instead of improvising around it. diff --git a/docs/research/2026-08-07-ap159-s1b-contract.md b/docs/research/2026-08-07-ap159-s1b-contract.md new file mode 100644 index 00000000..da03d554 --- /dev/null +++ b/docs/research/2026-08-07-ap159-s1b-contract.md @@ -0,0 +1,111 @@ +# 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 1–4 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 **310127–310257**. + 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 **309968–310126**) also contains a + box refinement block (@0x0052c76c–0x0052c7d2). 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.