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>
215 lines
6.7 KiB
Markdown
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.
|