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>
408 lines
20 KiB
Markdown
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.
|