acdream/docs/research/2026-07-19-r6-update-object-order-pseudocode.md
Erik c5ab99081c fix(motion): restore retail object manager order
Process animation completion at the retail process_hooks boundary, then run targeting, movement, PartArray completion, and PositionManager in the named UpdateObjectInternal order for local, remote, hidden, and position-less animated objects. Retire TS-42 with deterministic conformance coverage.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
2026-07-19 21:40:14 +02:00

6.7 KiB

R6 retail object-update order

Date: 2026-07-19 Retail oracle: Asheron's Call v11.4186 (September 2013 EoR)

Scope

This note pins the control-flow order needed by R6. It deliberately separates two kinds of animation-hook work:

  • control hooks such as AnimDoneHook, whose result changes the motion queues observed later in the same object update; and
  • pose-dependent presentation hooks such as particles and sounds, whose App-layer delivery may wait until the final root/part pose has been published without changing motion state.

That split is an acdream presentation boundary. It does not change retail's logical hook order.

Named-retail sources

  • CPhysicsObj::process_hooks 0x00511550
  • CPhysicsObj::UpdatePositionInternal 0x00512C30
  • CPhysicsObj::UpdateObjectInternal 0x005156B0
  • CPhysicsObj::update_object 0x00515D10
  • CPartArray::AnimationDone 0x00517D30
  • CPartArray::HandleMovement 0x00517D60
  • AnimDoneHook::Execute 0x00526C20
  • CPhysicsObj::Hook_AnimDone 0x0050FDA0

Primary text: docs/research/named-retail/acclient_2013_pseudo_c.txt. Addresses and names are also pinned in docs/research/named-retail/symbols.json.

Corrected top-level order

An older roadmap diagram put process_hooks after the manager tail. The named retail function body disproves that. UpdatePositionInternal processes hooks before it returns to UpdateObjectInternal; the transition/commit and every manager therefore observe completed animation state in the same object tick.

CPhysicsObj::update_object
  -> quantum gate/subdivision
  -> CPhysicsObj::UpdateObjectInternal
       -> CPhysicsObj::UpdatePositionInternal
            -> CPartArray::Update
            -> PositionManager::adjust_offset
            -> Frame::combine
            -> CPhysicsObj::UpdatePhysicsInternal
            -> CPhysicsObj::process_hooks       <-- completion drain
       -> transition / SetPositionInternal
       -> DetectionManager::CheckDetection
       -> TargetManager::HandleTargetting
       -> MovementManager::UseTime
       -> CPartArray::HandleMovement
       -> PositionManager::UseTime
       -> ParticleManager::UpdateParticles
       -> ScriptManager::UpdateScripts

Pseudocode

CPhysicsObj::update_object

if parent != null or cell == null or state contains Frozen:
    return

refresh active/view-distance state
elapsed = current_physics_time - last_update_time

if elapsed > HugeQuantum:
    last_update_time = current_physics_time
    return

while elapsed > MaxQuantum:
    last_update_time += MaxQuantum
    UpdateObjectInternal(MaxQuantum)
    elapsed -= MaxQuantum

if elapsed > MinQuantum:
    last_update_time = current_physics_time
    UpdateObjectInternal(elapsed)

The comparisons are strict (>), not >=. A sub-minimum remainder stays in the clock for a later call.

CPhysicsObj::UpdatePositionInternal(dt, candidate)

delta = identity Frame

if state does not contain Hidden:
    if part_array != null:
        part_array.Update(dt, delta)

    if transient state contains OnWalkable:
        delta.origin *= object_scale
    else:
        delta.origin = zero

if position_manager != null:
    position_manager.adjust_offset(delta, dt)

candidate = combine(current_frame, delta)

if state does not contain Hidden:
    UpdatePhysicsInternal(dt, candidate)

process_hooks()

Hidden suppresses PartArray time advance and physics integration. It does not suppress PositionManager::adjust_offset, hook processing, or the later manager tail.

CPhysicsObj::process_hooks

execute each queued physics hook in FIFO order
clear physics-hook queue

execute each queued animation hook in FIFO order
clear animation-hook queue

For an AnimDoneHook:

AnimDoneHook::Execute(owner)
  -> owner.Hook_AnimDone()
  -> owner.part_array.AnimationDone(success = true)
  -> MotionTableManager.AnimationDone(true)
  -> possibly MotionDone on the matching pending motion

Consequently, targeting and movement managers in the same object tick must see the post-completion queue state.

CPhysicsObj::UpdateObjectInternal(dt) manager tail

candidate = current frame
UpdatePositionInternal(dt, candidate)

if candidate moved:
    transition/sweep from current frame to candidate
    commit accepted position and collision response

if detection_manager != null:
    detection_manager.CheckDetection()

if target_manager != null:
    target_manager.HandleTargetting()

if movement_manager != null:
    movement_manager.UseTime()

if part_array != null:
    part_array.HandleMovement()
    // tailcalls MotionTableManager.UseTime /
    // CheckForCompletedMotions

if position_manager != null:
    position_manager.UseTime()

if particle_manager != null:
    particle_manager.UpdateParticles(dt)

if script_manager != null:
    script_manager.UpdateScripts(dt)

The tail is reached for both visible and Hidden objects when the object quantum runs. A missing manager is simply skipped. No manager is reordered around another manager based on local/remote/player/NPC classification.

acdream conformance boundary

The first R6 cutover uses one explicit coordinator for the ordered manager tail. Local and remote owners provide callbacks for the managers they possess; unsupported DetectionManager is represented by an absent callback rather than an invented behavior.

Animation-hook capture performs AnimationDone control effects immediately, at the process_hooks boundary. Pose-dependent routing remains deferred until the current frame's root/part poses are published. Deferred routing must never invoke AnimationDone a second time and must never own MotionTableManager.UseTime; that belongs only to the PartArray manager-tail slot.

Conformance cases

  1. Animation completion is visible to TargetManager and MovementManager in the same object tick.
  2. Target runs before Movement; Movement runs before PartArray; PartArray runs before PositionManager.
  3. A missing manager does not shift or duplicate another callback.
  4. Hidden objects skip sequence advance but retain the same manager tail.
  5. A visual hook sees a pose published after capture, while an AnimDoneHook changes motion state at capture time.
  6. Deferred visual-hook drain never completes an animation twice and never performs a zero-tick motion sweep.
  7. A sub-MinQuantum frame performs no object-tail call; accumulated time is consumed by a later quantum.

Cross-reference notes

ACE's physics/motion ports remain interpretation aids for manager ownership and state naming; the retail binary above is authoritative for order. The existing RemoteChaseEndToEndHarnessTests hand-codes acdream's pre-R6 order and is therefore a migration target, not an oracle.