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>
123 lines
6.7 KiB
Markdown
123 lines
6.7 KiB
Markdown
# #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.
|