acdream/docs/research/2026-08-07-330-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.7 KiB

#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 DatCollectionSetup 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 (LiveEntitySessionControllerDatLiveEntityProjectionMaterializer, 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.