# #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.