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

215 lines
6.7 KiB
Markdown

# 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.
```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
```
## Pseudocode
### `CPhysicsObj::update_object`
```text
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)`
```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
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`:
```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. 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.