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>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal file
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue