# 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: - the immediate **`AnimDoneHook` control path**, whose result changes the motion queues observed later in the same object update; and - **all other captured animation hooks**, which acdream currently defers until the final root/part pose has been published. This includes presentation hooks such as particles and sounds, but also semantically relevant `CallPES`, default-script chaining, and translucency hooks. That split is an acdream presentation boundary. Authored FIFO order is preserved within acdream's deferred non-`AnimationDone` stream, but moving that stream past retail's manager tail changes manager-relative timing; that is the registered TS-50 divergence. ## Named-retail sources - `CPhysicsObj::process_hooks` `0x00511550` - `CPhysics::UseTime` `0x00509950` - `CPhysicsObj::animate_static_object` `0x00513DF0` - `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. ```text 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 CPhysics::UseTime -> ordinary object table in the order above -> static_animating_objects -> CPhysicsObj::animate_static_object -> CPartArray::Update -> UpdatePartsInternal -> UpdateChildrenInternal -> ScriptManager::UpdateScripts -> ParticleManager::UpdateParticles -> CPhysicsObj::process_hooks ``` ## Pseudocode ### `CPhysicsObj::update_object` ```text if parent != null or cell == null or state contains Frozen: clear Active transient state return refresh active/view-distance state elapsed = current_physics_time - last_update_time if elapsed <= FrameEpsilon (0.000199999995 seconds): last_update_time = current_physics_time return 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 `>=`. Elapsed time at or below the roughly 0.2-millisecond frame epsilon is explicitly rebased/discarded. Above that epsilon, a remainder at or below `MinQuantum` stays in the clock for a later call; `MaxQuantum` controls subdivision and `HugeQuantum` controls stale discard. These are separate gates. `UpdateObjectInternal` then branches on the transient Active bit. An inactive object skips position integration, hook processing, Detection, Target, Movement, PartArray, and PositionManager, but still advances its ParticleManager and ScriptManager tails. A Hidden object that remains Active is different: it enters `UpdatePositionInternal`, suppresses only PartArray advance and physics integration, and retains PositionManager adjustment, hook processing, and the complete manager tail. ### `CPhysicsObj::UpdatePositionInternal(dt, candidate)` ```text 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` ```text for each physics hook present in the linked list walk: completed = hook.Execute(owner) if completed: unlink and destroy that hook else: retain it for a later object update for i from zero while i < current animation-hook count: animation_hooks[i].Execute(owner) // the bound is live: a hook appended during execution can run in this drain shrink animation-hook storage and set the count to zero ``` The physics-hook walk snapshots each current node's `next` pointer before execution. A completed hook is removed; an incomplete hook remains linked. The animation-hook loop deliberately rereads `m_num` after each callback, so reentrant append is consumed in the same drain. Neither collection is an unconditional snapshot-and-clear queue. For an `AnimDoneHook`: ```text 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 ```text 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 and the object is Active. An inactive object skips this position/manager body but still reaches Particle then Script. A missing manager is simply skipped. No manager is reordered around another manager based on local/remote/player/NPC classification. ### `CPhysicsObj::animate_static_object` ```text if cell == null: return elapsed = current_time - update_time if elapsed <= FrameEpsilon (0.000199999995 seconds): update_time = current_time return if elapsed > HugeQuantum: update_time = current_time return if part_array != null: if state contains HasDefaultAnim: part_array.Update(elapsed) rotate root by the current angular vector UpdatePartsInternal() UpdateChildrenInternal() if state contains HasDefaultScript and script_manager != null: script_manager.UpdateScripts() if particle_manager != null: particle_manager.UpdateParticles() process_hooks() update_time = current_time ``` The static workset has no ordinary-object `MinQuantum` remainder gate or `MaxQuantum` subdivision loop: it uses only the frame epsilon and `HugeQuantum`, then passes the complete admitted elapsed time to PartArray. It therefore has a distinct Script → Particle → hooks tail, not ordinary objects' Particle → Script tail. acdream currently shares one ParticleSystem and one PhysicsScriptRunner across every owner and advances them once after the complete ordinary/static worksets. That is the registered TS-51 adaptation and must not be silently reordered by an ownership-only extraction. ## 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. Every other captured animation hook currently remains deferred until the final root/part poses are published, including particle/audio/light/translucency hooks, `CallPES`, and default-script chaining. 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. Parented, cell-less, and Frozen objects return before clock admission. 5. Inactive objects skip position/hooks/manager time but retain Particle then Script; Hidden + Active objects skip sequence/physics but retain adjustment, hooks, and the manager tail. 6. A non-`AnimationDone` hook sees a pose published after capture, while an `AnimDoneHook` changes motion state at capture time. 7. Deferred hook drain never completes an animation twice and never performs a zero-tick motion sweep; representative `CallPES`, default-script, and translucency hooks retain their current deferred path. 8. An incomplete physics hook remains linked, and an animation hook appended by another animation hook executes in the same retail drain. 9. Static objects retain their distinct Script → Particle → hooks order. 10. Ordinary epsilon/Min/Max/Huge boundaries and static epsilon/Huge boundaries preserve their separate strict retail comparisons. ## Cross-reference notes ACE's `PhysicsObj.cs` corroborates ordinary Particle → Script and static Script → Particle → hooks order. It does **not** corroborate the retail clock or hook-lifetime gates: ACE uses its server `TickRate`/Min/Max policy and clears physics hooks unconditionally, unlike retail's epsilon/Huge behavior and completion-based hook retention. ACViewer exposes the same client-shaped methods and manager order but shares ancestry with ACE, so that agreement is corroboration rather than a second oracle. holtburger's native protocol crates have no client physics-loop equivalent (its vendored ACViewer copy is not independent). The retail binary above is authoritative for every boundary. The existing `RemoteChaseEndToEndHarnessTests` hand-codes acdream's pre-R6 order and is therefore a migration target, not an oracle.