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>
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)
LiveEntityCollisionBuilder+LiveEntityDefaultPoseResolverare already presentation-free logic: their imports are Core, Core.Net (WorldSession.EntitySpawn), Core.World (WorldEntity), DatReaderWriter. The ONLY App coupling is theLiveEntityRecord exactRecordidentity-guard parameter onBuild(...)and theAcDream.App.Worldusing it drags in.- The headless host HAS all required content:
HeadlessSessionWorldProjectionholds_content.Dats(fullDatCollection—Setupand MotionTable DBObjs resolvable) and_content.PreparedCollision→ per-landblockPhysicsDataCachewith flat GfxObj physics (GetFlatGfxObj). - Once a shadow IS registered, movement follows automatically:
RuntimeRemotePhysicsUpdater(Runtime, both hosts) already callsShouldSynchronizeShadowat line ~844 on its tick. Registration is the whole gap. - The no-window inbound entity route is
RuntimeLiveEntitySessionController(src/AcDream.Runtime/Session/), constructed byHeadlessSessionHost(~line 682). Per AD-64 the graphical host runs a PARALLEL App-side route (LiveEntitySessionController→DatLiveEntityProjectionMaterializer, which calls_collisionBuilder.Build(...)at ~line 832 andLiveEntityCollisionBuilder.Registerat ~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. - The graphical local-player disposition
(
SessionPlayerComposition~line 573) choosesRegisteredAuthoredPayloadvsProvenShapelessbyPhysicsEngine.ShadowObjects.HasLogicalOwner(key.LocalEntityId). The headless pin (HeadlessSessionHost~line 637) hardcodesProvenShapeless.
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
SetupDBObj and default pose, call the hoistedBuild(...)with the setup's OWN part ids aseffectivePartGfxObjIds(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), andRegisterinto the engine'sShadowObjects. - 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):
- 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 inShadowObjects.GetObjectsInCellfor its cell, thenResolveWithTransitiona 0.48 m mover into it and assert the move is blocked/adjusted — the end-to-end "bot cannot walk through an NPC" fact. - Teardown row: despawn removes the shadow registration.
- 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
ProvenShapelesspin (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/ShadowShapeBuilderinternals.
Acceptance
dotnet build AcDream.slnx -c Releasegreen.- Full suite
dotnet test AcDream.slnx -c Release -m:1green — 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.