feat(physics): port canonical retail set-position core

This commit is contained in:
Erik 2026-07-31 20:44:03 +02:00
parent 6b28ff999c
commit e84a388e6f
6 changed files with 1952 additions and 21 deletions

View file

@ -0,0 +1,142 @@
# Canonical retail `SetPosition` — placement/streaming Slice 4A
## Scope
This note pins the pure physics half of the placement/streaming closeout.
Slice 4A lands the retail placement transaction as a separately testable Core
mechanism. It deliberately does **not** replace the production snap-only
resolver yet: Runtime lost-cell ownership and the complete inbound-route
cutover remain Slice 4B. AP-1 and AD-1 therefore remain active until that
cutover is complete.
Named-retail oracle, Sept 2013 EoR:
- `CPhysicsObj::SetPosition` `0x005160C0`
- `CPhysicsObj::SetPositionInternal` `0x00515BD0`
- `CPhysicsObj::AdjustPosition` `0x00511D80`
- `CPhysicsObj::CheckPositionInternal` `0x00511E90`
- `CTransition::find_valid_position` `0x0050C310`
- `CTransition::find_placement_position` `0x0050C170`
- `CTransition::find_placement_pos` `0x0050BA50`
- `CTransition::validate_placement_transition` `0x0050ADC0`
- `CTransition::validate_placement` `0x0050B210`
- `CPhysicsObj::ForceIntoCell` `0x00515660`
- `CPhysicsObj::handle_all_collisions` `0x00514780`
## Retail transaction
```text
SetPosition(request):
transition = makeTransition() // GENERAL_FAILURE if none
init_object(transition, object)
if the PartArray has no spheres:
init_sphere(1, dummy center=(0,0,0.1), radius=0.1, scale=1)
else:
init_sphere(first min(count,2) authored spheres, exact object scale)
if flags & RANDOM_SCATTER (0x200):
return scatter only
result = SetPositionInternal(request)
if result != OK and flags & SCATTER (0x100):
return scatter
return result
SetPositionInternal(request):
AdjustPosition(request frame, first sphere, noCreate=(flags & 0x20))
if no resident cell:
store the adjusted authoritative frame and enter lost-cell lifetime
return OK
if the live weenie is Hook, Storage, or Corpse:
return ForceIntoCell(resident cell, frame)
set do_not_load_cells from flag 0x20
if !CheckPositionInternal(...):
handled = handle_all_collisions(...)
return handled ? COLLIDED : NO_VALID_POSITION
if transition.curr_cell == null:
return NO_CELL
commit the complete transition
return OK
```
`AdjustPosition` branches on the claimed cell shape. A direct outdoor claim
runs pure `LandDefs::adjust_to_outside` normalization before visible-cell
lookup. An indoor claim first resolves the visible cell and child; only a
resident indoor cell marked `seen_outside` falls back to outdoor
normalization. An absent indoor cell (including the `0xFFFF` sentinel) remains
the exact claimed cell/frame. A map-edge outdoor normalization failure stores
cell zero with the otherwise unchanged frame. The old `max(terrainZ, z)` lift
and nearest-in-Z scan do not occur in this canonical mechanism.
`CheckPositionInternal` calls the complete placement transition. Without the
slide flag, retail accepts the result only when signed
`resolvedX-requestedX <= 0.0500000007`, the same signed Y condition holds, and
the cell is unchanged. Z is not part of that predicate. The resolved origin is
accepted while the requested orientation remains intact.
## Two validators, not one
The similarly named retail helpers have different contracts and stay separate
in the port:
- `validate_placement_transition` is the inner `find_placement_pos` validator.
Any non-OK state from `COLLIDED` through `SLID`, when sliding is permitted,
resets `COLLISIONINFO`; it never retries placement.
- `validate_placement` is the outer initial/final validator. Only `ADJUSTED`
or `SLID`, and only while its retry argument is true, performs one
`placement_insert`; `COLLIDED` neither resets nor retries.
Step-down is disabled only for missiles. For fewer than two spheres retail
first clamps the requested height to half the radius when the sphere diameter
is less than or equal to that height. It then performs one full probe when the
diameter is greater than the resulting height, otherwise two half probes.
Equality belongs to the two-half-probe branch.
## Modern seam
`PhysicsEngine.SetPosition` returns one immutable
`PhysicsSetPositionResult`. `SetPositionError` retains the header values
(`OK=0`, `GENERAL=1`, `NO_VALID=2`, `NO_CELL=3`, `COLLIDED=4`,
`INVALID_ARGS=0x100`) while `PhysicsResidenceDisposition` separately reports
`Committed`, `DeferredCell`, or `Unchanged`. Missing content is therefore
successful-but-deferred, never misreported as a placement failure.
The result carries the complete commit packet: root/cell-local frame,
contact/walkable/water state, sliding and collision normals, stationary-fall
counter, a complete immutable `COLLISIONINFO` snapshot for the real
`handle_all_collisions` callback (including contact/last-contact, sliding,
collision normal, stationary-fall, environment, adjustment, and object
fields), the callback result, and an explicit shadow action. A PhysicsBSP or changed-cell force commit
requests canonical shadow recalculation; a non-BSP transition replaces its
shadows only when the transition produced a nonempty cell array, otherwise it
preserves the prior list. Unchanged force placement changes only the frame.
Core never creates cells synchronously, so retail flag `0x20`
(`DoNotCreateCells`) has no differential loader branch inside this pure
mechanism. Both flag states can only observe already-published immutable cell
content and otherwise return `DeferredCell`. Carrying the flag into exact-cell,
generation-scoped async admission is part of the still-open Slice 4B
adaptation tracked with AD-2; this slice does not claim a dead SpherePath field
as exact behavior.
The existing public `Resolve` compatibility entry remains entirely unchanged
for production movement and zero-delta callers. Its result cannot represent a
successful-but-deferred residence, so hiding `DeferredCell` inside
`ResolveResult.Ok` would corrupt the contract. Slice 4B will route every
authoritative placement family through `SetPosition`, atomically install its
packet, and own exact lost-cell wakeup/commit.
## Automated oracle
`PhysicsSetPositionTests` pins error values, absent/invalid outdoor and indoor
claims, cross-landblock frame normalization, map-edge failure and the `0xFFFF`
sentinel, dummy/authored sphere setup, nonpositive scale, Ethereal seeding,
explicit-only PathClipped, missile step-down, exact equality schedules, both validators,
the late compass sample's float bits, signed no-slide behavior, actual
collision-handler mapping, force-class policy, explicit shadow actions, null
current-cell wakeup, ten-record scratch exhaustion, exact scatter ordering,
failed-probe scratch lifetime, and deferred-scatter stop. The legacy public
Resolve fixture remains unchanged until Slice 4B.