acdream/docs/research/2026-07-31-runtime-set-position-collision-reporting.md

182 lines
8 KiB
Markdown

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