feat(runtime): own SetPosition collision reports

This commit is contained in:
Erik 2026-08-01 00:15:11 +02:00
parent ec627c13a2
commit 237d1184d2
19 changed files with 3744 additions and 103 deletions

View file

@ -252,15 +252,21 @@ prepared-mover caching, and caps synchronous Scatter/RandomScatter work at 64
attempts; this keeps valid authored retail request shapes while rejecting a
hostile `uint.MaxValue` loop at the authority boundary.
Two boundaries intentionally remain open for 4B2:
One authority boundary intentionally remains open for 4B2:
- `RuntimePortalPlacementAuthority` validates immutable token shape only;
4B2 must bind it to active `RuntimeWorldTransitState` generation, teleport
sequence, destination, and host acknowledgement before reveal.
- Runtime applies the canonical collision-report state but fails the handler
return closed. Retail returns report/track success, not collision presence;
the per-object report/tracking owner required for that boolean is not yet in
Runtime, and the former environment/object-presence guess is forbidden.
The former collision-report boundary is closed by
`RuntimeCollisionReportingState`. Runtime now owns retail's exact-key object
contact table, environment latch, strict ordinary/ethereal expiry, force-end,
static and `ReportAsEnvironment` routing, reciprocal callback eligibility,
missile-state clearing, ordered reentrant dispatch, and the report-result
boolean which distinguishes placement `Collided` from `NoValidPosition`.
Successful SetPosition commits reporting after Contact/OnWalkable and ground
callbacks but before its single physical response and shadow reflood. See
`docs/research/2026-07-31-runtime-set-position-collision-reporting.md`.
### Slice 4B2 checkpoint 1 — public dormant host seam
@ -284,13 +290,13 @@ preparation followed by one validated atomic body/controller publication.
Either choice belongs to the all-route ownership cutover, not this narrow
dormant-seam checkpoint.
This is still a deliberately non-activating checkpoint. Production spawn,
This remains a deliberately non-activating checkpoint. Production spawn,
Position, projectile, drop/pickup/parent, and portal routes do not submit to
the dormant SetPosition owner yet. The cutover remains blocked on the real
retail collision report/tracking return, exact ordered Setup spheres/scale/
step heights/flags/cell-local preparation, presentation-only rebucketing, and
placement-prefix quiescence before collision retirement. AP-1 and AD-1 remain
open until those prerequisites and every production route land together.
the dormant SetPosition owner yet. The cutover remains blocked on exact
ordered Setup spheres/scale/step heights/flags/cell-local preparation,
presentation-only rebucketing, and placement-prefix quiescence before
collision retirement. AP-1 and AD-1 remain open until those prerequisites and
every production route land together.
AD-2 remains the explicit async adaptation: collision readiness can publish in
a different frame from retail's blocking load. A failed wake is safely re-

View file

@ -1,5 +1,11 @@
# Remaining physics-divergence campaign handoff — 2026-07-31
> **Checkpoint 2 update:** Slice 4B2 prerequisite A, Runtime SetPosition
> collision-report ownership, is implemented in the next checkpoint. Continue
> with the dedicated
> [`runtime SetPosition collision-reporting handoff`](2026-07-31-runtime-set-position-collision-reporting-handoff.md),
> not the prerequisite-A instructions preserved below as historical context.
## Purpose and stopping point
This is the deliberate handoff boundary requested after placement Slice 4B2

View file

@ -0,0 +1,243 @@
# Runtime SetPosition collision-report ownership handoff - 2026-07-31
## Purpose and exact stopping point
This handoff records placement Slice 4B2 checkpoint 2: the isolated Runtime
owner for retail SetPosition collision tracking and report-result semantics.
The checkpoint intentionally stops before authored mover preparation, shared
local-controller body publication, graphical/headless placement projection,
collision-prefix quiescence, or any production SetPosition route cutover.
Production behavior is therefore unchanged by this checkpoint. The new owner
is populated only by the dormant `RuntimeSetPositionState` and focused tests.
AP-1 and AD-1 remain narrowed/open; AP-22 and AD-10 remain open.
## Exact workspace
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
- Branch: `codex/port-claude-agents`
- Starting checkpoint: `ec627c13`
(`docs(physics): hand off remaining divergence campaign`)
- This handoff belongs to the same behavior commit as the implementation.
- No upstream is configured for this worktree branch.
- Remotes:
- `origin`: `https://git.snakedesert.se/erik/acdream.git`
- `github`: `git@github.com:eriknihlen/acdream.git`
Continue in this worktree unless the user explicitly requests otherwise.
`AGENTS.md` has an unrelated pre-existing content diff and must not be staged,
restored, or rewritten as part of this checkpoint. Several other paths report
line-ending/stat noise without a content diff; stage only the exact paths
listed in the final commit.
## Retail oracle
The complete readable oracle is
[`2026-07-31-runtime-set-position-collision-reporting.md`](2026-07-31-runtime-set-position-collision-reporting.md).
The named-retail anchors are:
- `CPhysicsObj::report_object_collision_end` `0x00510A90`
- `CPhysicsObj::report_environment_collision` `0x00512FC0`
- `CPhysicsObj::report_object_collision` `0x00513060`
- `CPhysicsObj::track_object_collision` `0x00513F10`
- `CPhysicsObj::report_collision_start` `0x00513FD0`
- `CPhysicsObj::report_collision_end` `0x00514620`
- `CPhysicsObj::handle_all_collisions` `0x00514780`
- successful `CPhysicsObj::SetPositionInternal(CTransition const*)`
`0x00515330`
- `CPhysicsObj::leave_world` `0x005155A0`
- placement failure in `CPhysicsObj::SetPositionInternal` `0x00515BD0`
The source is `docs/research/named-retail/acclient_2013_pseudo_c.txt`; the
struct authority is `docs/research/named-retail/acclient.h`.
## What this checkpoint implements
`RuntimeCollisionReportingState` is the sole per-session owner of:
- one environment-collision latch per exact `RuntimeEntityKey`;
- one ordered object-contact table per exact owner incarnation;
- retained peer server GUID, touch time, and ethereal-at-touch state;
- static and `ReportAsEnvironment` routing;
- asymmetric `IgnoreCollisions` and reciprocal `ReportCollisions` eligibility;
- strict ordinary `age > 1.0` and ethereal `age > 0.0` expiry;
- force-end-before-callback mutation for reentrant safety;
- missing-peer self-only end reports without resolving a later GUID reuse;
- exact `Missile | AlignPath | PathClipped` clearing on the canonical record,
borrowed body, retained shadow state, and mutation version;
- a monotonic immutable report FIFO with observer-failure isolation;
- the retail callback-eligibility boolean used by failed placement to choose
`Collided` versus `NoValidPosition`;
- terminal ownership diagnostics and deterministic session/disposal cleanup.
Successful dormant SetPosition commits contact, water/walkable and ground
edges first, runs reporting next, applies physical response once, and then
refloods the shadow. An intervening Vector or Movement update suppresses only
the stale physical response; it does not erase collision tracking or reports.
Failed placement always supplies retail's `previousContact = false` and
`previousOnWalkable = false`, reports once, applies its one response pass, and
maps the report result exactly.
Hidden, teleport/withdrawal, deletion, session reset, and disposal use distinct
lifetime edges. Leaving the world force-ends the departing owner's table but
retains its environment latch and incoming peer records. Destruction then
forgets only the departing owner state. Other owners retain exact-key contacts
until their own expiry/force pass and can emit a missing-target end using the
preserved server GUID. Hidden and session-clear paths force-end while the old
report flags and bodies are still eligible, before state/reset teardown.
## Architectural boundaries
- Runtime owns all canonical collision-report state and report-result logic.
- Core exposes only the exact successful SetPosition ordering seam and the
retained-shadow collision identity required by Runtime.
- App and Headless gain no report table, queue, heuristic, or production
placement consumer.
- Reports are presentation-free and keyed by exact Runtime identity.
- Network/update callbacks may re-enter, but every later mutation revalidates
current identity, body, and the relevant authority version.
- Physical-response velocity authority is deliberately separate from report
authority, matching retail's ordering without overwriting a newer vector.
## Validation and independent review
The saved final diff passed:
- combined focused Runtime collision-report and SetPosition tests: 76/76;
- complete Runtime project: 562/562;
- graphical/headless Runtime-physics ownership and dormancy guards: 4/4;
- focused Core SetPosition/contact/response ordering tests: 29/29;
- complete Core project: 4,224 passed / 1 intentional skip;
- from-source Release solution rebuild: 0 errors and 21 pre-existing test-
project nullable/analyzer warnings; this checkpoint introduces none;
- complete Release solution: 10,309 passed / 4 intentional skips;
- warmed steady-contact refresh: 0 managed bytes across 10,000 calls;
- warmed immediate dormant SetPosition commit: still below the existing
2,048-byte-per-operation ceiling, with no new captured-delegate cost;
- architecture/adversarial re-review: clean after fixing Hidden/session/delete
reentrancy, stale shadow-state authority, allocation churn, and batch cost;
- retail-conformance re-review: clean against every named address above.
The final retail re-review found and closed two last ordering defects before
sign-off: object collision now snapshots the mover's Missile bit before the
source callback and, when that snapshot was set, unconditionally masks the
current `Missile | AlignPath | PathClipped` bits afterward. Thus an ordinary
callback-added Missile is retained when the mover was not previously a missile,
but a callback which clears Missile and re-adds path bits cannot evade the
pre-gated retail mask. Environment collision retains retail's post-callback
current-Missile test. Successful SetPosition now
publishes reports before installing the new stationary-fall counter, applies
the physical response next, installs StationaryFall/Stop/Stuck transient bits
after response, and only then refloods the shadow.
The host guard reads both production source trees. It proves App and Headless
borrow `GameRuntime.EntityObjects.Physics`, declare no second collision table
or return heuristic, and still contain no placement-channel consumer. No
connected/live gate is required for this dormant checkpoint because no
production route can populate or publish the new report owner.
## Exact implementation and test paths
The behavior commit containing this handoff changes exactly these ten code and
test paths:
- `src/AcDream.Core/Physics/PhysicsObjUpdate.cs`
- `src/AcDream.Core/Physics/ShadowObjectRegistry.cs`
- `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs`
- `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs`
- `src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs`
- `src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs`
- `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs`
- `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs`
- `tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs`
- `tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs`
The same commit synchronizes the architecture, divergence register, canonical
SetPosition research, roadmap, milestones, project memory, prior campaign
handoff pointer, retail-oracle note, and this detailed handoff. `AGENTS.md` and
the pre-existing line-ending/stat-noise paths are deliberately excluded.
## Remaining work - required order
### 1. Exact authored mover preparation
Build every SetPosition request from Setup's ordered authored spheres, exact
scale/presence semantics, StepUp/StepDown heights, flags, cell-local frame and
orientation, and current position/vector/state authority versions. Do not
synthesize a cylinder from visual radius/height or pre-mutate canonical state.
### 2. Atomic local-controller/body publication
Prepare off-canonical, then perform one Runtime-validated atomic transaction
which publishes the exact same body to graphical and no-window controllers.
Every body writer, remote/projectile binding, SetPosition operation, clock
epoch, deletion, reset and disposal path must participate. Do not resurrect
the rejected snapshot/rollback lease documented in the prior handoff.
### 3. Presentation-only host projection
Implement graphical and headless observers over the existing dormant placement
receipt channel. Withdraw removes presentation/spatial consumers while
retaining Runtime identity; Place projects only the immutable committed frame;
Discard retires the older revision. Host failure retries the exact FIFO head
and never rolls Runtime back.
### 4. Collision-prefix quiescence and atomic route activation
Park SetPosition residents before retiring their collision prefix, publish the
complete replacement generation, wake exact matching residents, and cut every
spawn/Position/portal/projectile/drop/pickup/parent/delete route over together.
Only then may AP-1 and AD-1 retire.
### 5. Remaining campaign slices
- Port retail-authored object collision shape precedence and retire AP-22.
- Remove remote terrain-normal preprojection and let the transition resolver
use the retained contact plane, retiring AD-10.
- Run the full automated and connected matrix, update all ledgers, and close
the remaining physics campaign only with direct evidence.
## Rollback
This checkpoint is one bisectable commit. Revert the commit containing this
file to remove collision-report ownership without disturbing the earlier
SetPosition residence and receipt-channel checkpoints. Do not revive the old
collision-presence guess or the rejected body snapshot lease.
Because a Git commit cannot embed its own final hash, resolve the exact
checkpoint and revert command without ambiguity using:
```powershell
$checkpoint = git log -1 --format=%H -- `
docs/research/2026-07-31-runtime-set-position-collision-reporting-handoff.md
git show --stat $checkpoint
git revert $checkpoint
```
Earlier rollback points remain:
```powershell
git revert 270f5154 # dormant public placement receipt channel
git revert 4c02ac42 # Runtime SetPosition/lost-cell residence owner
git revert e84a388e # pure Core retail SetPosition transaction
```
## Resume procedure
1. Continue in
`C:\Users\erikn\.codex\worktrees\af5e\acdream` and verify
`git branch --show-current` reports `codex/port-claude-agents`.
2. Resolve the exact checkpoint with the `git log` command above and confirm
it is the current `HEAD` before starting the next behavior slice.
3. Read `AGENTS.md`, `docs/architecture/acdream-architecture.md`, this file,
the collision-report oracle, the canonical SetPosition research, and the
prior remaining-campaign handoff completely.
4. Run `git status --short`. Preserve the unrelated `AGENTS.md` content diff
and every documented line-ending/stat-noise path. Never stage by blanket.
5. Begin only **Exact authored mover preparation**, the first remaining item
above. Do not activate production routes, retire AP-1/AD-1, begin AP-22 or
AD-10, or resurrect the rejected body snapshot/rollback lease.
6. Use exact-path staging and rerun the matching focused projects,
`dotnet build AcDream.slnx -c Release --nologo`, and
`dotnet test AcDream.slnx -c Release --no-build --nologo` before the next
reviewed checkpoint.

View file

@ -0,0 +1,182 @@
# Runtime SetPosition collision-report ownership
**Scope:** placement/streaming Slice 4B2 prerequisite A only. This closes the
missing Runtime owner for retail collision tracking and the boolean returned by
`CPhysicsObj::handle_all_collisions`. It does **not** activate any graphical or
headless production SetPosition route.
## Named-retail oracle
Primary sources:
- `CPhysicsObj::report_object_collision_end` `0x00510A90`
- `CPhysicsObj::report_environment_collision` `0x00512FC0`
- `CPhysicsObj::report_object_collision` `0x00513060`
- `CPhysicsObj::track_object_collision` `0x00513F10`
- `CPhysicsObj::report_collision_start` `0x00513FD0`
- `CPhysicsObj::report_collision_end` `0x00514620`
- `CPhysicsObj::handle_all_collisions` `0x00514780`
- `CPhysicsObj::SetPositionInternal(CTransition const*)` `0x00515330`
- `CPhysicsObj::leave_world` `0x005155A0`
- placement failure path in `CPhysicsObj::SetPositionInternal` `0x00515BD0`
- `CPhysicsObj::CollisionRecord`, `EnvCollisionProfile`,
`ObjCollisionProfile`, and `AtkCollisionProfile` in
`docs/research/named-retail/acclient.h`
The source text is
`docs/research/named-retail/acclient_2013_pseudo_c.txt`. The addresses above
are the behavioral authority; the older unnamed chunks remain fallback only.
### Environment reporting
```text
report_environment_collision(meInContact):
reported = false
if !colliding_with_environment:
if self.ReportCollisions && self.weenie != null:
DoCollision(EnvCollisionProfile(self.velocity, meInContact))
reported = true
colliding_with_environment = true
if self.Missile:
self.state &= ~(Missile | AlignPath | PathClipped)
return reported
```
The latch is independent of callback eligibility. An object with no collision
callback still latches its environment contact, and a repeated environment hit
returns false. Retail has no environment-end callback. `leave_world` does not
clear this latch; the next `handle_all_collisions` call re-arms it only after a
non-environment frame.
### Object reporting and tracking
```text
track_object_collision(other, meInContact):
if other.Static:
return report_environment_collision(meInContact)
record = { touched_time = PhysicsTimer.curr_time,
ethereal = other.Ethereal }
existed = collision_table.clobber(other.id, record)
if existed:
return false
return report_object_collision(other, meInContact)
```
The table insert/refresh precedes callbacks. Duplicate contacts refresh their
time but never replay a start callback. DAT/static classification and physics
state come from the exact shadow object which produced the collision; object-ID
presence is not a valid substitute.
`report_object_collision` first maps `ReportAsEnvironment` to the environment
path. Otherwise:
- the mover reports only when the other object is not `IgnoreCollisions` and
the mover has `ReportCollisions` plus a weenie;
- a mover which had Missile set before the source callback unconditionally
masks its current `Missile | AlignPath | PathClipped` bits after striking a
non-ignored object, even when the callback cleared Missile but re-added path
bits; when pre-callback Missile was clear, callback-added Missile is retained;
- the reciprocal report occurs only when the other has `ReportCollisions`, the
mover is not `IgnoreCollisions`, and the other has a weenie;
- the return is true when at least one callback is attempted. It is never a
collision-presence boolean.
### Expiry and end reporting
`report_collision_end(force)` removes records before dispatching callbacks.
This ordering is required for safe reentrancy.
```text
ordinary record: remove when age > 1.0, or force
ethereal record: remove when age > 0.0, or force
```
Equality remains alive. A still-resolvable non-`ReportAsEnvironment` peer may
receive reciprocal collision-end callbacks. When the peer no longer resolves,
the owner can still receive its self-only end using the stored retail object
ID. A later incarnation must never satisfy the old contact record.
### `handle_all_collisions` and SetPosition ordering
```text
handle_all_collisions(info, previousContact, previousOnWalkable):
reported = false
for other in info.collidedObjects, in encounter order:
reported |= track_object_collision(other, previousContact)
report_collision_end(force = false)
if environment latch is already set:
latch = info.collided_with_environment
else if info.collided_with_environment
|| (!previousOnWalkable && self.OnWalkable):
reported |= report_environment_collision(previousContact)
apply retail collision velocity/stationary response
return reported
```
Successful `SetPositionInternal(CTransition const*)` commits the resolved
cell/frame, Contact/WaterContact/OnWalkable state, and HitGround/LeaveGround
edge before `handle_all_collisions`; it ignores the returned boolean and only
then replaces/refloods shadows. Collision reports observe the old stationary-
fall state; the new counter is installed before physical response, while the
StationaryFall/Stop/Stuck transient bits are replaced after response and before
shadow reflood. The placement failure path calls
`handle_all_collisions(info, false, false)` and maps true to
`SetPositionError::Collided` (`4`) and false to `NoValidPosition` (`2`).
Consequently acdream must keep report/tracking separate from the physical
response: failed placement runs both once, while successful Runtime commit
runs reporting between the contact/ground commit and shadow reflood without
double-applying velocity response.
## Runtime ownership contract
The implementation is presentation-free and belongs to the per-session
`RuntimePhysicsState` graph. Its invariants are:
- owner and peer identities are exact `RuntimeEntityKey` values, not server
GUID or local ID alone;
- each tracked record retains the peer server GUID, touch time in the Runtime
simulation-clock domain, and ethereal-at-touch bit;
- collided IDs and authored/static ownership are admitted through the exact
retained `ShadowObjectRegistry` registration which produced the collision;
every dynamic Static/Ethereal/Ignore/ReportAsEnvironment decision then reads
the current canonical `PhysicsBody.State`, never a stale shadow snapshot;
- immutable reports preserve encounter order and dispatch through a retained,
reentrancy-safe FIFO;
- callback exceptions are isolated, while the retail report-result boolean is
determined by callback eligibility and does not depend on subscribers;
- every callback boundary revalidates the exact record/body/authority before
any later canonical mutation;
- force-end mutates the complete expired set before publishing ends; exact-key
admission guards prevent callback reentry from recreating a leaving owner,
and session teardown blocks the whole owner batch before its first callback;
- one source lifetime token covers a complete precollected end batch, so a
callback-accepted delete stops every later peer report even while teardown
sidecars remain resolvable;
- lifetime forget, session reset, and disposal cannot donate state to GUID
reuse;
- terminal ownership diagnostics include contact/report state and converge to
zero;
- graphical and no-window hosts borrow the same Runtime owner. No host owns a
second collision table or report-result heuristic.
The warmed steady-contact refresh path allocates zero managed bytes. Expired
contact storage is allocated lazily only after the first actual expiry, and
session-batch teardown is linear in owner count.
## Deliberately deferred
The canonical SetPosition owner remains dormant in production. The following
belong to later 4B2 commits and are not part of this checkpoint:
- exact ordered Setup spheres, authored scale and step-height preparation;
- the atomic shared local-controller body transaction;
- presentation-only rebucketing and placement-prefix quiescence;
- graphical/headless spawn, Position, portal, projectile, drop, pickup,
parent, and delete route cutover.
AP-1 and AD-1 therefore remain open, narrowed only by removal of the
collision-report prerequisite.