acdream/docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

408 lines
20 KiB
Markdown

# R6 complete root Frame cutover — retail pseudocode
Date: 2026-07-19
Scope: the translation and orientation emitted by `CSequence` and consumed by
`CPhysicsObj::UpdatePositionInternal`. This note replaces the former acdream
split between animation root translation and command-derived/manual rotation.
## Named retail oracle
- `Frame::combine` `0x005122E0` (`acclient_2013_pseudo_c.txt:280355`)
- `Frame::set_rotate` `0x00535080`
- `Frame::IsValid` `0x00534ED0`
- `Position::subtract2` (complete relative Position/Frame)
- `CPhysicsObj::UpdatePositionInternal` `0x00512C30` (`:280817`)
- `CPhysicsObj::UpdateObjectInternal` `0x005156B0`
- `CPhysicsObj::SetPositionInternal(CTransition const*)` `0x00515330`
- `CPhysicsObj::change_cell` `0x00513390`
- `CPhysicsObj::remove_shadows_from_cells` `0x00511230`
- `CPhysicsObj::add_shadows_to_cells` `0x00514AE0`
- `CPhysicsObj::update_object` `0x00515D10`
- `CPhysicsObj::prepare_to_enter_world` `0x00511FA0`
- `CPhysicsObj::set_active` `0x0050FC40`
- `CPhysicsObj::animate_static_object` `0x00513DF0`
- `CPhysics::UseTime` `0x00509950` (separate static-object loop)
- `CPartArray::Update` `0x00517DB0` (`:285883`)
- `add_motion` `0x005224B0` (`:298437`)
- `CSequence::apply_physics` `0x00524AB0` (`:300955`)
- `InterpolationManager::InterpolateTo` `0x00555B20`
- `InterpolationManager::adjust_offset` `0x00555D30`
The installed retail portal DAT is a second executable fixture. Motion table
`0x09000001` (Humanoid), global `TurnRight` modifier `0x0000000D`, has
`HasOmega` and literal omega `(0, 0, -1.5)` radians/second. It is not
`-pi/2`, and it is not absent. The fixture is pinned by
`HumanoidMotionTableRootMotionTests`.
ACE cross-reference:
`docs/research/2026-07-02-r2-motiontable/r2-ace-motiontable.md` records the
ACE `MotionTable`/`Sequence` implementation: modifier lookup tries the styled
key then the unstyled key, and `add_motion` writes
`MotionData.Velocity * speed` plus `MotionData.Omega * speed`. ACE agrees with
the named client here. Retail remains the ordering and transform oracle.
## Pseudocode
```text
add_motion(sequence, motionData, speed):
if motionData is null:
return
sequence.velocity = motionData.velocity * speed
sequence.omega = motionData.omega * speed
for animation in motionData.animations:
sequence.append_animation(animation * speed)
apply_physics(sequence, frame, magnitudeQuantum, signSource):
signedQuantum = abs(magnitudeQuantum)
if signSource < 0:
signedQuantum = -signedQuantum
frame.origin += sequence.velocity * signedQuantum
frame.rotate(sequence.omega * signedQuantum)
CPartArray.Update(dt, localFrame):
sequence.update(dt, localFrame)
CPhysicsObj.prepare_to_enter_world(now):
update_time = now
remove object and children from lost/destroy queues
if not Static:
transient_state.Active = true
CPhysicsObj.update_object(now):
if parent exists or cell is null or Frozen:
transient_state.Active = false
return without consuming update_time
if player_object exists:
player_vector = offset(player.position, object.position)
player_distance = length(player_vector)
if player_distance <= 96 world units or partArray is null:
set_active(true)
else:
transient_state.Active = false
// With no player_object, retain the prior Active state.
elapsed = now - update_time
if elapsed <= F_EPSILON or elapsed > HugeQuantum:
update_time = now
return
while elapsed > MaxQuantum:
UpdateObjectInternal(MaxQuantum)
update_time += MaxQuantum
elapsed -= MaxQuantum
if elapsed > MinQuantum:
UpdateObjectInternal(elapsed)
update_time = now
// A remainder at or below MinQuantum stays pending.
CPartArray.InitDefaults(setup):
if setup.DefaultAnimation != 0:
sequence.clear_animations()
sequence.append_animation(
animation = setup.DefaultAnimation,
low = 0,
high = -1,
framerate = 30)
owner.InitDefaults(setup)
CPhysicsObj.InitDefaults(setup):
if setup.DefaultScript != 0:
play_script_internal(setup.DefaultScript)
install setup default motion/sound/physics-script tables
if state has Static:
if setup.DefaultAnimation != 0:
state |= HasDefaultAnim
if setup.DefaultScript != 0:
state |= HasDefaultScript
if state has HasDefaultAnim or HasDefaultScript:
CPhysics.AddStaticAnimatingObject(this)
CPhysicsObj.animate_static_object(now):
if cell is null:
return
elapsed = now - update_time
if elapsed <= F_EPSILON or elapsed > 2 seconds:
update_time = now
return
if partArray exists:
if state has HasDefaultAnim:
partArray.Update(elapsed, null)
// Retail passes the stored vector directly; no elapsed multiply.
position.frame.grotate(angularVelocity)
UpdatePartsInternal()
UpdateChildrenInternal()
if state has HasDefaultScript and scriptManager exists:
scriptManager.UpdateScripts()
if particleManager exists:
particleManager.UpdateParticles()
process_hooks()
update_time = now
CPhysics.UseTime(now):
for object in ordinary object table:
object.update_object(now)
for object in static_animating_objects:
object.animate_static_object(now)
// These are separate ownership/workset paths. InitDefaults registers any
// physics-Static object carrying Setup DefaultAnimation/DefaultScript,
// including a server-created live object.
CPhysicsObj.UpdatePositionInternal(dt, outputWorldFrame):
localFrame = identity
if not Hidden:
if partArray exists:
partArray.Update(dt, localFrame)
if OnWalkable:
localFrame.origin *= objectScale
else:
localFrame.origin *= 0
// Orientation is never cleared or scaled by this gate.
if positionManager exists:
positionManager.adjust_offset(localFrame, dt)
outputWorldFrame = combine(position.frame, localFrame)
if not Hidden:
UpdatePhysicsInternal(dt, outputWorldFrame)
process_hooks()
Frame.combine(output, current, delta):
output.origin = current.origin + rotate(current.orientation, delta.origin)
output.set_rotate(current.orientation * delta.orientation)
Frame.set_rotate(candidate):
previous = orientation
orientation = normalize(candidate)
if not Frame.IsValid():
orientation = previous
InterpolationManager.adjust_offset(delta, dt):
if no Position node or body is not in Contact:
return without mutating delta
relative = Position.subtract2(target, current)
relative.origin = clamp_to_catchup_distance(relative.origin, dt)
if manager.keep_heading:
relative.frame.set_heading(0)
delta = relative.frame // replaces both origin and orientation
CPhysicsObj.SetPositionInternal(transition):
if transition.current_cell is null:
prepare_to_leave_visibility()
store_position(transition.current_position)
object_maintenance.goto_lost_cell()
clear Active
return
if current cell differs:
change_cell(transition.current_cell)
else:
commit the new full objcell id to the object, PartArray, and children
set_frame(transition.current_position.frame)
commit contact plane, OnWalkable, sliding normal, and collision callbacks
if transition has a non-empty crossed-cell array:
remove_shadows_from_cells()
add_shadows_to_cells(transition.cell_array)
```
The ordering consequence is important: a delta translates through the old
orientation and only then applies its relative rotation. A large elapsed frame
must run multiple complete object quanta in time order. A render fragment below
the strict minimum does not advance CSequence at all; it stays in the object
clock until a later admitted quantum.
## Port mapping
- `MotionDeltaFrame.Combine` implements the retail composition and scales only
the incoming origin.
- One incarnation-stable `RetailObjectQuantumClock` is shared by local,
remote, Hidden, and projectile components of each live object. It admits
complete object quanta; the local animation callback writes one reusable
`MotionDeltaFrame` only after admission. `prepare_to_enter_world` clears the
retained fragment and immediately restores Active only for non-Static
objects, so their next elapsed frame can run; Static objects retain the
inactive state established by initialization/leave-visibility. It does not
invent a second reactivation frame.
- `CPartArray::InitDefaults` installs Setup DefaultAnimation for every
setup-backed PartArray, independent of Static. Static controls only the
additional `static_animating_objects -> animate_static_object` workset.
Both DAT-hydrated statics and server-created objects whose final physics
state is Static enter that workset when their Setup carries a
DefaultAnimation. Default animation bypasses the MotionTable and installs
the Setup animation directly over `0..last` at 30 frames/second. A short
cell-less interval remains on the owner's clock and catches up after
re-entry; an interval above two seconds is rebased and discarded. Script-only
static owners still use acdream's shared PhysicsScriptRunner and are covered
by TS-51 rather than entering an empty animation scan.
A live static workset entry binds the exact PartArray and PhysicsBody already
owned by its `LiveEntityRecord`; it never constructs a second sequencer or
body. The raw angular-velocity vector is applied without multiplying by
elapsed time, exactly as the named function does. A changed root commits the
entity, effect pose, and multipart collision shadow together. Zero omega does
not trigger a collision reflood, and a Hidden/nonresident object cannot have
its suspended shadow restored by this animation path.
- Animation hooks are captured after `UpdatePhysicsInternal` and before the
transition/manager tail. Semantic `AnimationDone` executes there, matching
`CPhysicsObj::process_hooks`. Other pose-dependent router sinks remain
deferred until final root/part/equipped-child pose publication; that known
residual is registered as TS-50 rather than claimed as exact ordering.
Static owners retain their semantic hook tail until final root, part, and
attached-child publication, matching `animate_static_object`'s later
`process_hooks` call. A residency change invalidates both the prepared pose
and that pending tail.
- Retail session dispatch and `CPhysics::UseTime` are single-threaded FIFO
operations. App therefore wraps complete packet and object-frame operations
in `RetailInboundEventDispatcher`: a callback-triggered packet queues behind
the current operation instead of recursively interleaving with its tail.
Position, State, Vector, and Movement keep separate authority versions;
Position/Vector/Movement additionally share only a velocity-write version.
Never replace these with one global body-mutation token: that incorrectly
discards independent accepted channels (for example State during Position).
The direct packet/frame path carries state into a cached static callback and
allocates nothing; only genuinely nested work receives a retained FIFO node.
- Projectile `BeginQuantum` is candidate-only. It restores the canonical begin
frame/dynamics before returning; `CompleteQuantum` may commit only while the
exact live record, projectile runtime, entity, spatial owner, and prediction
version remain current. Any accepted Position, Vector, or State invalidates
the candidate without rolling back the authoritative mutation.
- Animation-hook batches capture the pose owner's monotonic lifetime before
running semantic AnimationDone. Capture rechecks it before every completion,
and drain rechecks that lifetime and root pose before every routed hook,
because either AnimationDone or an earlier sink can delete the owner and
reuse the same local ID synchronously.
- Remote animation advances into one reusable DAT `Frame`; both components are
passed to `RemotePhysicsUpdater` and applied through the same
`PositionManager` frame.
- A body-backed animated object without a MovementManager now commits its
complete Frame through `LiveEntityOrdinaryPhysicsUpdater`: root translation
is admitted only while OnWalkable, orientation remains live, the resulting
candidate runs through Transition/SetPositionInternal, and the same record is
rebucketed without recreating resources. Callback deletion/replacement is
revalidated before every commit.
- The resolved body and `WorldEntity` root/full-cell frame are committed before
the canonical rebucket callback, matching SetPositionInternal's frame/contact
commit before its final shadow replacement. A loaded-to-pending rebucket (or
same-GUID replacement during its visibility callback) invalidates the caller
before shadow or manager-tail publication. Non-projectile shadow residency is
symmetric in `LiveEntityPresentationController`, including the pending-first
case whose initial false edge precedes collision registration; projectiles
retain their dedicated residency owner.
- Local projection and both authoritative remote UpdatePosition tails use the
same post-rebucket exact-owner gate. Remote collision refresh is forced by a
cell change or by a changed complete pose (translation or sign-invariant
quaternion), so offset/multipart Setup shapes rotate in place and a same-pose
cell crossing cannot retain an old flood seed.
- A retained projectile remains the same CPhysicsObj when Missile clears. With
no MovementManager it continues through the ordinary one-sphere transition
owner; with a MovementManager the owner transfers exactly once to remote
motion. Projection re-entry restores its retained collision shadow on the
visibility edge, before any later simulation quantum.
- Active interpolation preserves the target's complete quaternion and replaces
the PartArray Frame. The already-close `InterpolateTo` branch applies only
the target heading, matching `CPhysicsObj::set_heading`. `keep_heading` is
manager-wide, not a per-node flag; each near enqueue replaces it.
- `FrameOps.SetRotate` rejects zero, non-finite, and otherwise invalid
quaternion candidates by restoring the prior orientation.
- Spawn, teleport, correction, and outbound movement/position serialization
read the authoritative body quaternion directly; none round-trips through
the horizontal `Yaw` projection.
- `MotionTableDispatchSink` only dispatches motion-table commands. It no longer
reports turn callbacks.
- `RemoteMotion.ObservedOmega`, the player/NPC multiplication-order fork, and
`AnimationSequencer.SetCycle`'s synthetic `pi/2` turn omega are deleted.
- A controller deliberately constructed without a `PartArray` retains a
Humanoid-only fallback at the DAT-pinned `1.5` radians/second. A normal GUI
client does not enter that path.
## Conformance gates
- `MotionDeltaFrameTests`: exact `FrameOps.Combine` equivalence, scale affects
origin only, and temporal turn-then-move composition.
- `RetailObjectQuantumClockTests`: strict bounds, retained remainder,
MaxQuantum subdivision, HugeQuantum discard, `set_active` rebasing, and
Static-aware `prepare_to_enter_world` reset.
- `RetailObjectActivityGateTests` and `LiveEntityRuntimeTests`: the 96-unit
PartArray gate, parent/cell/Frozen suspension, loaded rebucket retention,
pending re-entry rebasing, body/clock Active synchronization, and GUID-reuse
clock isolation.
- `LiveEntityAnimationSchedulerTests`: the canonical live-object workset also
advances objects without a render animation, consumes their queued near
UpdatePosition correction, applies complete generic root
Frames, transfers a retained missile body exactly once when classification
clears, and stops catch-up immediately when a callback deletes, withdraws,
or replaces an incarnation.
- `RetailStaticAnimatingObjectSchedulerTests` and
`AnimationSequencerTests`: unconditional Setup DefaultAnimation installation,
DAT/live physics-Static workset membership, direct 30-fps playback,
short-residency-gap catch-up, canonical PartArray/body reuse, raw (not
elapsed-scaled) omega, synchronized multipart shadow rotation, zero-omega
no-op commits, callback-safe removal, and the retail greater-than-two-second
discard. `StaticLiveRootCommitter` coverage proves Hidden omega cannot
resurrect a suspended shadow.
- `RemoteInboundMotionDispatcherTests`: one animation-optional owner preserves
retail packet-head/style/type-0/sticky ordering with a null PartArray sink;
unknown movement types execute the head and never enter the case-0 funnel;
supersession during interrupt prevents every later packet side effect.
- `LiveEntityOrdinaryPhysicsUpdaterTests`: complete root Frames cross cells and
landblocks through Transition, while hook-side deletion prevents stale body,
shadow, pose, or spatial commits.
- `ProjectileControllerTests`: pending-cell hydration restores the same body's
Active/InWorld/shadow residency immediately and does not replay logical
creation; the destination cell is canonical before suspension and after
hydration, and authoritative Position/Vector/State between prediction halves
discards only the stale candidate.
- `RetailInboundEventDispatcherTests`, `LiveEntityPresentationControllerTests`,
`RemoteTeleportControllerTests`, and `EntityEffectPoseRegistryTests`: nested
packet arrival drains after the complete older tail, independent State and
Position both survive resolver callbacks, newer Position supersedes older
placement, Hidden-to-Visible recursion remains FIFO, and hook batches cannot
cross deletion/local-ID reuse during capture or drain. The FIFO's nonnested
state path is pinned at zero allocations after warmup.
- `PlayerMovementControllerTests`: full-frame local composition, large-frame
equivalence, hidden/airborne gates, hook cadence, and proof that
animation-backed turns are not integrated twice.
- `InterpolationManagerTests`: complete relative target orientation,
keep-heading, and already-close heading-only behavior.
- `RemotePhysicsUpdaterTests` and `LiveEntityPresentationControllerTests`:
translation uses the pre-turn orientation and the same frame then rotates the
body; cross-cell loaded-to-pending commits the destination root before the
callback boundary, suspends rather than re-adds collision, restores on
hydration, and isolates old shadows across callback GUID reuse. Direct
authoritative rebuckets and initial-pending materialization cover the same
symmetric collision-residency contract; same-position rotation and cell-only
changes refresh collision while unchanged/sign-flipped quaternion poses do
not reflood.
- `LiveEntityRuntimeTests`: update iteration and render-ID classification copy
only concrete spatial animation owners through reusable storage with zero
steady-state allocations.
- `MotionTableDispatchSinkTests`: Branch-4 turn preserves the locomotion cycle,
contributes literal DAT omega, and unwinds on stop.
- `RemoteChaseEndToEndHarnessTests`: turn, chase, re-arm, action completion, and
sticky following run through the complete Frame plus retail manager tail.
- `HumanoidMotionTableRootMotionTests`: installed retail walk displacement and
exact Humanoid `TurnRight` omega `-1.5` radians/second.
- `MovementManagerTests` and `PlayerMovementControllerTests`:
`PerformMovement` invokes `set_active(1)` before request validation, while a
Static physics object preserves retail's no-op activation behavior.
## Registered residual
The ordinary/static object worksets and their root/animation clocks are now
retail-shaped, but acdream's shared particle and PhysicsScript managers still
advance once per render frame after every owner has run. Retail advances those
tails inside each admitted object update (and inside
`animate_static_object`). This timing difference is deliberately registered as
TS-51; it is not part of the R6 conformance claim.
Final automated gate (2026-07-20): three independent retail-conformance,
architecture/integration, and adversarial re-reviews reported no actionable
finding after correction; Release build completed with zero warnings; the full
solution passed 6,446 tests with 5 intentional skips.