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

20 KiB

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

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.