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>
This commit is contained in:
parent
0f996db747
commit
c5ab99081c
17 changed files with 493 additions and 98 deletions
215
docs/research/2026-07-19-r6-update-object-order-pseudocode.md
Normal file
215
docs/research/2026-07-19-r6-update-object-order-pseudocode.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue