feat(physics): port retail complete object frame pipeline

Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-20 09:10:31 +02:00
parent 31a0889f08
commit f961d70023
77 changed files with 12513 additions and 1871 deletions

View file

@ -4,6 +4,15 @@
**Source:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR PDB-named decomp).
**Cross-check:** `src/AcDream.Core/Physics/InterpolationManager.cs` (current port), `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` (cdb-traced).
> **2026-07-19 correction:** the May audit correctly identified that
> `adjust_offset` mutates its `Frame*`, but incorrectly described the result as
> translation-only and the old `Vector3` API as equivalent. Retail preserves
> the complete relative quaternion produced by `Position::subtract2` unless
> `keep_heading` explicitly replaces that rotation with heading zero. R6 now
> carries the complete Frame. See
> `docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md`. Section 7 is
> retained as a historical snapshot of the May port, not current status.
---
## 1. Class layout (verbatim from `acclient.h`)
@ -135,7 +144,9 @@ Our current port does duplicate-prune against only the last entry (`_queue.Last`
## 4. `InterpolationManager::adjust_offset` @ `0x00555d30`
Signature: `void adjust_offset(InterpolationManager *this, Frame *arg2, double arg3)`
- `arg2` = output Frame (already-zeroed identity Frame from caller `CPhysicsObj::UpdatePartsInternal` @ `0x00512c3c`).
- `arg2` = the mutable per-object delta Frame. `CPartArray::Update` may already
have written root translation and orientation into it; active interpolation
replaces that complete Frame.
- `arg3` = frame `dt` in seconds (double).
### Critical answer: **arg2 is MUTATED IN PLACE.** Not a delta-return.
@ -146,13 +157,18 @@ The last meaningful action (line 353253) is:
00555f10 Frame::operator=(arg2, &__return);
```
where `__return` is a Frame whose `m_fOrigin` was just scaled to the per-frame step, and whose rotation is 0 (or kept-heading) (line 353251). The caller composes this into the world position with:
where `__return` is the complete relative Frame from `Position::subtract2`.
Only `m_fOrigin` is scaled to the per-frame step. Its quaternion remains the
target-relative rotation unless `keep_heading` calls `Frame::set_heading(0)`.
The caller composes this into the world position with:
```c
00512d22 Frame::combine(arg3 /*world out*/, &this->m_position.frame, &var_40 /*= arg2*/);
```
**So `adjust_offset` writes a translation-only Frame — the caller treats it as an offset Frame to combine with the body's current position.** Our acdream port returns a `Vector3` delta, which the caller adds to the body — equivalent semantics.
**So `adjust_offset` writes a complete relative Frame, and the caller combines
both its translation and orientation with the body's current Frame.** A
`Vector3`-only port loses pitch/roll and is not equivalent.
### Step-by-step retail flow:
@ -238,7 +254,7 @@ if (this->keep_heading != 0) {
}
// ---- Output ----
Frame::operator=(arg2, &delta_frame); // OUT: arg2 = translation-only Frame
Frame::operator=(arg2, &delta_frame); // OUT: complete relative Frame
```
### NOTE on `progress_quantum`
@ -456,7 +472,7 @@ if (CPhysicsObj::SetPositionSimple(po, &target, 1) == OK_SPE) {
| **Stall fail action** | Increments fail counter; only blips when threshold exceeded INSIDE adjust_offset | Calls `NodeCompleted(0)` on stall and pops the head; UseTime does the actual blip | **Architectural gap.** Retail's NodeCompleted(0) on stall pops the head node (advancing the queue) — useful when the head is unreachable but later nodes might be. Our port leaves the head in place. This means a single bad waypoint can cause repeated 5-frame failures rather than skipping past it. |
| Blip target on threshold | `_queue.Last.TargetPosition` (tail) | UseTime: tail OR blipto_position OR last-pos-before-velocity-tail | Mostly OK for our use case (only type-1 nodes). |
| `transient_state & 1` gate | None | Required | Probably safe in our codebase; file as follow-up. |
| AdjustOffset return shape | `Vector3` delta | In-place mutate `Frame*` (translation-only Frame) | Functionally equivalent — caller composes with current position either way. |
| AdjustOffset return shape | `Vector3` delta | In-place mutate complete `Frame*` | **Historical R6 gap, fixed 2026-07-19.** The old shape lost target-relative orientation; production now uses `MotionDeltaFrame`. |
### Concrete actionable changes

View file

@ -44,9 +44,9 @@ stage; **LOW** = dormant/textual.
| G3 | **update_internal boundary model: clamp-to-`high_frame`/`low_frame` + leftover-time carry, boundary test `floor(frameNum) > high_frame` (i.e. ≥ high+1).** acdream tests `newPos >= maxBoundary - FrameEpsilon` and clamps `_framePosition = maxBoundary - FrameEpsilon` — epsilon-shifted boundary, different clamp target, no strict retail equivalence at exact-integer landings. This is the #61 "link→cycle boundary flash" bug class. | `0x005255d0` body (lines 301839-302235); ACE `Sequence.cs:366-377` (fwd), `:394-406` (rev) | `AnimationSequencer.cs:894,900` (`maxBoundary - FrameEpsilon`) | **BLOCKER** |
| G4 | **`safety=64` loop cap** — retail has none; termination comes from `frameTimeElapsed` shrinking + the `frametime == 0` branch returning. Mandate says delete bandaids on cutover. | ACE `Sequence.cs:351-443` (no cap); decomp `0x005255d0` (while-true loop) | `AnimationSequencer.cs:872-874` | MED (delete in R1 core) |
| G5 | **AnimDone gate is a LIST-STRUCTURE test, not a node flag.** Retail queues the global `anim_done_hook` singleton when `animDone && hook_obj != null && (anim_list.head_ 4) != first_cyclic` — i.e. "the old head has been consumed and we're not already in the cyclic tail". acdream pushes `AnimationDoneSentinel` gated on `!_currNode.Value.IsLooping` — a per-node flag that acdream itself invented (retail nodes have no IsLooping). Different gate ⇒ different MotionDone timing. R2's `CheckForCompletedMotions → AnimationDone → MotionDone` chain consumes this signal; it must be retail-exact in R1. | `0x00525943-0x00525968` (verified raw this pass); AnimDoneHook singleton at data `0x0081d9fc`, Execute `0x00526c20``Hook_AnimDone 0x0050fda0``CPartArray::AnimationDone(1)` (§18a) | `AnimationSequencer.cs:1129` (`AnimationDoneSentinel`), Advance's `!IsLooping` gate (r1-acdream map row "AnimationDone hook") | **HIGH** |
| G6 | **Two-stage hook dispatch.** Retail `execute_hooks` QUEUES matching `CAnimHook*` into `CPhysicsObj.anim_hooks` (SmartArray); `CPhysicsObj::process_hooks` executes them inside `UpdatePositionInternal`, before the transition and manager tail, then resets `m_num=0`. acdream's `_pendingHooks` + `ConsumePendingHooks()` is structurally similar. R6 processes semantic `AnimationDone` at capture time while retaining pose-dependent presentation delivery until current root/part poses are published. | `execute_hooks 0x00524830` (line 300780), `add_anim_hook 0x00514c20` (line 282906), `process_hooks 0x00511550` (line 279431) (§18); corrected order pinned in `2026-07-19-r6-update-object-order-pseudocode.md` | `AnimationSequencer.cs` pending-hook queue; `AnimationHookFrameQueue.Capture` semantic completion and `Drain` presentation delivery | RETIRED by R6 semantic-order cutover 2026-07-19 |
| G7 | **`apply_physics` + Frame-target root motion is unwired.** Retail: `update(quantum, Frame*)` accumulates `velocity*signed_quantum` into `frame.m_fOrigin` and `omega*signed_quantum` via `Frame::rotate`, with `signed_quantum = copysign(fabs(arg3), arg4)`; per crossed frame the quantum is `1.0/framerate` signed by elapsed. acdream has NO `apply_physics`: velocity/omega are surfaced as `CurrentVelocity`/`CurrentOmega` properties consumed by external movement code, and pos-frame root motion accumulates into `_rootMotionPos/_rootMotionRot` that **nothing drains** (`ConsumeRootMotionDelta()` has zero callers). | `0x00524ab0` (line 300955, §19); `Frame::rotate 0x004525b0` | `AnimationSequencer.cs:975-979` region (`ConsumeRootMotionDelta`, dead); `CurrentVelocity/CurrentOmega` consumers `GameWindow.cs:9331-9334`, `:12917` | **HIGH** — this is retail's entire physics-from-animation mechanism; R6's `CPartArray.Update → adjust_offset → Frame.combine` tick order needs it |
| G8 | **Empty-list physics-only fallback.** Retail `update`: if `anim_list` empty and `frame != null``apply_physics(frame, elapsed, elapsed)` (accumulated velocity still moves the object — free-fall/knockback with no anim). acdream `Advance` with no `_currNode` does nothing. | `0x00525b80` (line 302402, §22) | `AnimationSequencer.cs:874` (`while (... && _currNode != null ...)` — falls out) | MED |
| G6 | **Two-stage hook dispatch.** Retail `execute_hooks` QUEUES matching `CAnimHook*` into `CPhysicsObj.anim_hooks` (SmartArray); `CPhysicsObj::process_hooks` executes the complete stream inside `UpdatePositionInternal`, before the transition and manager tail, then resets `m_num=0`. acdream's `_pendingHooks` + `ConsumePendingHooks()` is structurally similar. R6 processes semantic `AnimationDone` at capture time while retaining every other pose-dependent hook until current root/part/equipped-child poses are published. | `execute_hooks 0x00524830` (line 300780), `add_anim_hook 0x00514c20` (line 282906), `process_hooks 0x00511550` (line 279431) (§18); corrected order pinned in `2026-07-19-r6-complete-root-frame-pseudocode.md` | `AnimationSequencer.cs` pending-hook queue; `AnimationHookFrameQueue.Capture` semantic completion and `Drain` presentation delivery | **PARTIAL:** AnimationDone semantic order retired 2026-07-19; non-AnimationDone delivery remains TS-50 |
| G7 | **Frame-target root motion.** `update(quantum, Frame*)` accumulates literal velocity and omega plus authored PosFrames into one Frame. Local and remote movement now pass that complete Frame through `PositionManager`, `Frame::combine`, and collision; command-derived translation and manual rotation consumers are gone. | `0x00524ab0` (line 300955, §19); `Frame::rotate 0x004525b0`; `Frame::combine 0x005122E0` | `CSequence.ApplyPhysics`; `AnimationSequencer.Advance(dt, Frame)`; `MotionDeltaFrame`; `PlayerMovementController`; `RemotePhysicsUpdater`; `2026-07-19-r6-complete-root-frame-pseudocode.md` | **RETIRED 2026-07-19** |
| G8 | **Empty-list physics-only fallback.** Retail `update`: if `anim_list` is empty and `frame != null``apply_physics(frame, elapsed, elapsed)` (accumulated velocity still moves the object — free-fall/knockback with no animation). The ported `CSequence.Update` implements that branch and `AnimationSequencer.Advance(dt, Frame)` exposes it to the ordinary-object scheduler. | `0x00525b80` (line 302402, §22) | `CSequence.Update`; `AnimationSequencer.Advance(dt, Frame)`; empty-list/full-Frame scheduler conformance tests | **RETIRED 2026-07-19** |
| G9 | **`advance_to_next_animation` uses elapsed-direction plus node-framerate sign gates.** For elapsed ≥ 0, subtract the outgoing pose only when its framerate is negative, step Next/wrap `first_cyclic`, and combine the incoming pose only when its framerate is positive. For elapsed < 0, subtract when outgoing framerate is non-negative, step Previous/wrap list tail, and combine when incoming framerate is negative. Physics inside a selected pose operation uses the separate strict `abs(framerate) > F_EPSILON` gate. Matching v11.4186 assembly and ACE agree; the earlier four unconditional pose ops cleanup was wrong. | `0x005252B0`; matching assembly audit 2026-07-19; `r1-ace-sequence.md:70-86` | `CSequence.AdvanceToNextAnimation` + exact-boundary/sign conformance tests | **RETIRED 2026-07-19** |
| G10 | **`append_animation` slides `first_cyclic` to the just-appended node on EVERY call.** Retail's "cyclic tail" is always exactly the LAST appended anim (so a multi-anim cycle MotionData loops only its final AnimData node once earlier ones are consumed). acdream sets `_firstCyclic` to the FIRST node of the cycle MotionData. Also retail: `if (curr_anim == null) { curr_anim = head; frame_number = get_starting_frame(head); }` — acdream's equivalents are scattered through `SetCycle`'s rebuild. | `0x00525510` (line 301777, §24) | `AnimationSequencer.cs:634-645` region + `EnqueueMotionData` (r1-acdream map rows "Node list", "add_motion") | **HIGH** — divergent loop membership for multi-anim cycles; also the retail invariant that makes remove_cyclic/apricot correct |
| G11 | **The remove-family with curr_anim snap semantics is missing.** Retail: `remove_cyclic_anims` (0x00524e40) deletes `first_cyclic`→tail, snapping `curr_anim` back to prev + `frame_number = get_ending_frame(prev)` (or 0), then `first_cyclic = tail`; `remove_link_animations(n)` (0x00524be0) / `remove_all_link_animations` (0x00524ca0) delete predecessors of `first_cyclic`, snapping `curr_anim` FORWARD to `first_cyclic` + `get_starting_frame`; `clear_animations` (0x00524dc0) full wipe; `apricot` (0x00524b40) trims consumed leading nodes after every update, bounded by `curr_anim`/`first_cyclic`. acdream instead has `ClearCyclicTail` + wholesale queue clears + the invented "stale-head `_currNode` force-relocation" + "Fix B link-skip" — all approximations of what the retail remove-family + apricot do naturally. | lines 301258/301060/301128/301207/300978 (§5-8, §20) | `AnimationSequencer.cs:1311` (`ClearCyclicTail`), `:511`, stale-head relocation + Fix B blocks in `SetCycle` (r1-acdream map rows "Stale-head handling", "Fix B") | **HIGH** — these retire two invented mechanisms |
@ -79,7 +79,7 @@ divergence-register row when touched.
| Per-frame crossing walk fires pose+hooks for EVERY integer frame crossed, strict ascending (fwd) / descending (rev) order | `0x005255d0` do/while loops (lines 302006-302056 + reverse mirror) | `AnimationSequencer.cs:910-941` (fwd `lastFrame++` w/ Forward, rev `lastFrame--` w/ Backward) |
| Forward node wrap to `first_cyclic` (loop-the-cycle mechanism) | `0x005252b0` @0x005253xx: `GetNext==null → first_cyclic` | `AnimationSequencer.cs:1350-1358` |
| Leftover-time carry into the next node after a boundary (multi-node fast-forward in one tick) | Matching v11.4186 assembly `0x00525982-0x00525990` copies `frameTimeElapsed` back then loops; ACE `Sequence.cs:436-442` agrees | `CSequence.UpdateInternal` assigns `timeElapsed = frameTimeElapsed` before looping |
| Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse | `Frame::combine`/`Frame::subtract1` call sites in `0x005255d0`/`0x005252b0` | `ApplyPosFrame(node, idx, reverse:)` fwd/conjugate-reverse (r1-acdream map row "Root motion") — values correct, TARGET wrong (G7: accumulator never drained) |
| Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse | `Frame::combine`/`Frame::subtract1` call sites in `0x005255d0`/`0x005252b0` | `CSequence` writes the caller-supplied root Frame; local and remote physics consume that same Frame (G7 retired) |
| `frame_number` floored to int for pose lookup (`get_curr_animframe`/`get_curr_frame_number` shape) | `0x00524970`/`0x005249d0` (§15/17) | `AnimationSequencer.cs:884` (`(int)Math.Floor(_framePosition)`) |
| `clear_physics` zeroing before rebuild | `0x00524d50` + `GetObjectSequence`'s `clear_physics; remove_cyclic_anims` pairing @0x005229cf etc. | `ClearPhysics()` called from `SetCycle` (r1-acdream map row "clear_physics") |
| `AnimData` speed scaling: only framerate × speed, low/high pass through | `operator* 0x00525d00` (invoked from `add_motion` @0x0052255b) | `LoadAnimNode` (`AnimData.Framerate * speedMod`) |

View file

@ -0,0 +1,408 @@
# R6 complete root Frame cutover — retail pseudocode
Date: 2026-07-19
Scope: the translation and orientation emitted by `CSequence` and consumed by
`CPhysicsObj::UpdatePositionInternal`. This note replaces the former acdream
split between animation root translation and command-derived/manual rotation.
## Named retail oracle
- `Frame::combine` `0x005122E0` (`acclient_2013_pseudo_c.txt:280355`)
- `Frame::set_rotate` `0x00535080`
- `Frame::IsValid` `0x00534ED0`
- `Position::subtract2` (complete relative Position/Frame)
- `CPhysicsObj::UpdatePositionInternal` `0x00512C30` (`:280817`)
- `CPhysicsObj::UpdateObjectInternal` `0x005156B0`
- `CPhysicsObj::SetPositionInternal(CTransition const*)` `0x00515330`
- `CPhysicsObj::change_cell` `0x00513390`
- `CPhysicsObj::remove_shadows_from_cells` `0x00511230`
- `CPhysicsObj::add_shadows_to_cells` `0x00514AE0`
- `CPhysicsObj::update_object` `0x00515D10`
- `CPhysicsObj::prepare_to_enter_world` `0x00511FA0`
- `CPhysicsObj::set_active` `0x0050FC40`
- `CPhysicsObj::animate_static_object` `0x00513DF0`
- `CPhysics::UseTime` `0x00509950` (separate static-object loop)
- `CPartArray::Update` `0x00517DB0` (`:285883`)
- `add_motion` `0x005224B0` (`:298437`)
- `CSequence::apply_physics` `0x00524AB0` (`:300955`)
- `InterpolationManager::InterpolateTo` `0x00555B20`
- `InterpolationManager::adjust_offset` `0x00555D30`
The installed retail portal DAT is a second executable fixture. Motion table
`0x09000001` (Humanoid), global `TurnRight` modifier `0x0000000D`, has
`HasOmega` and literal omega `(0, 0, -1.5)` radians/second. It is not
`-pi/2`, and it is not absent. The fixture is pinned by
`HumanoidMotionTableRootMotionTests`.
ACE cross-reference:
`docs/research/2026-07-02-r2-motiontable/r2-ace-motiontable.md` records the
ACE `MotionTable`/`Sequence` implementation: modifier lookup tries the styled
key then the unstyled key, and `add_motion` writes
`MotionData.Velocity * speed` plus `MotionData.Omega * speed`. ACE agrees with
the named client here. Retail remains the ordering and transform oracle.
## Pseudocode
```text
add_motion(sequence, motionData, speed):
if motionData is null:
return
sequence.velocity = motionData.velocity * speed
sequence.omega = motionData.omega * speed
for animation in motionData.animations:
sequence.append_animation(animation * speed)
apply_physics(sequence, frame, magnitudeQuantum, signSource):
signedQuantum = abs(magnitudeQuantum)
if signSource < 0:
signedQuantum = -signedQuantum
frame.origin += sequence.velocity * signedQuantum
frame.rotate(sequence.omega * signedQuantum)
CPartArray.Update(dt, localFrame):
sequence.update(dt, localFrame)
CPhysicsObj.prepare_to_enter_world(now):
update_time = now
remove object and children from lost/destroy queues
if not Static:
transient_state.Active = true
CPhysicsObj.update_object(now):
if parent exists or cell is null or Frozen:
transient_state.Active = false
return without consuming update_time
if player_object exists:
player_vector = offset(player.position, object.position)
player_distance = length(player_vector)
if player_distance <= 96 world units or partArray is null:
set_active(true)
else:
transient_state.Active = false
// With no player_object, retain the prior Active state.
elapsed = now - update_time
if elapsed <= F_EPSILON or elapsed > HugeQuantum:
update_time = now
return
while elapsed > MaxQuantum:
UpdateObjectInternal(MaxQuantum)
update_time += MaxQuantum
elapsed -= MaxQuantum
if elapsed > MinQuantum:
UpdateObjectInternal(elapsed)
update_time = now
// A remainder at or below MinQuantum stays pending.
CPartArray.InitDefaults(setup):
if setup.DefaultAnimation != 0:
sequence.clear_animations()
sequence.append_animation(
animation = setup.DefaultAnimation,
low = 0,
high = -1,
framerate = 30)
owner.InitDefaults(setup)
CPhysicsObj.InitDefaults(setup):
if setup.DefaultScript != 0:
play_script_internal(setup.DefaultScript)
install setup default motion/sound/physics-script tables
if state has Static:
if setup.DefaultAnimation != 0:
state |= HasDefaultAnim
if setup.DefaultScript != 0:
state |= HasDefaultScript
if state has HasDefaultAnim or HasDefaultScript:
CPhysics.AddStaticAnimatingObject(this)
CPhysicsObj.animate_static_object(now):
if cell is null:
return
elapsed = now - update_time
if elapsed <= F_EPSILON or elapsed > 2 seconds:
update_time = now
return
if partArray exists:
if state has HasDefaultAnim:
partArray.Update(elapsed, null)
// Retail passes the stored vector directly; no elapsed multiply.
position.frame.grotate(angularVelocity)
UpdatePartsInternal()
UpdateChildrenInternal()
if state has HasDefaultScript and scriptManager exists:
scriptManager.UpdateScripts()
if particleManager exists:
particleManager.UpdateParticles()
process_hooks()
update_time = now
CPhysics.UseTime(now):
for object in ordinary object table:
object.update_object(now)
for object in static_animating_objects:
object.animate_static_object(now)
// These are separate ownership/workset paths. InitDefaults registers any
// physics-Static object carrying Setup DefaultAnimation/DefaultScript,
// including a server-created live object.
CPhysicsObj.UpdatePositionInternal(dt, outputWorldFrame):
localFrame = identity
if not Hidden:
if partArray exists:
partArray.Update(dt, localFrame)
if OnWalkable:
localFrame.origin *= objectScale
else:
localFrame.origin *= 0
// Orientation is never cleared or scaled by this gate.
if positionManager exists:
positionManager.adjust_offset(localFrame, dt)
outputWorldFrame = combine(position.frame, localFrame)
if not Hidden:
UpdatePhysicsInternal(dt, outputWorldFrame)
process_hooks()
Frame.combine(output, current, delta):
output.origin = current.origin + rotate(current.orientation, delta.origin)
output.set_rotate(current.orientation * delta.orientation)
Frame.set_rotate(candidate):
previous = orientation
orientation = normalize(candidate)
if not Frame.IsValid():
orientation = previous
InterpolationManager.adjust_offset(delta, dt):
if no Position node or body is not in Contact:
return without mutating delta
relative = Position.subtract2(target, current)
relative.origin = clamp_to_catchup_distance(relative.origin, dt)
if manager.keep_heading:
relative.frame.set_heading(0)
delta = relative.frame // replaces both origin and orientation
CPhysicsObj.SetPositionInternal(transition):
if transition.current_cell is null:
prepare_to_leave_visibility()
store_position(transition.current_position)
object_maintenance.goto_lost_cell()
clear Active
return
if current cell differs:
change_cell(transition.current_cell)
else:
commit the new full objcell id to the object, PartArray, and children
set_frame(transition.current_position.frame)
commit contact plane, OnWalkable, sliding normal, and collision callbacks
if transition has a non-empty crossed-cell array:
remove_shadows_from_cells()
add_shadows_to_cells(transition.cell_array)
```
The ordering consequence is important: a delta translates through the old
orientation and only then applies its relative rotation. A large elapsed frame
must run multiple complete object quanta in time order. A render fragment below
the strict minimum does not advance CSequence at all; it stays in the object
clock until a later admitted quantum.
## Port mapping
- `MotionDeltaFrame.Combine` implements the retail composition and scales only
the incoming origin.
- One incarnation-stable `RetailObjectQuantumClock` is shared by local,
remote, Hidden, and projectile components of each live object. It admits
complete object quanta; the local animation callback writes one reusable
`MotionDeltaFrame` only after admission. `prepare_to_enter_world` clears the
retained fragment and immediately restores Active only for non-Static
objects, so their next elapsed frame can run; Static objects retain the
inactive state established by initialization/leave-visibility. It does not
invent a second reactivation frame.
- `CPartArray::InitDefaults` installs Setup DefaultAnimation for every
setup-backed PartArray, independent of Static. Static controls only the
additional `static_animating_objects -> animate_static_object` workset.
Both DAT-hydrated statics and server-created objects whose final physics
state is Static enter that workset when their Setup carries a
DefaultAnimation. Default animation bypasses the MotionTable and installs
the Setup animation directly over `0..last` at 30 frames/second. A short
cell-less interval remains on the owner's clock and catches up after
re-entry; an interval above two seconds is rebased and discarded. Script-only
static owners still use acdream's shared PhysicsScriptRunner and are covered
by TS-51 rather than entering an empty animation scan.
A live static workset entry binds the exact PartArray and PhysicsBody already
owned by its `LiveEntityRecord`; it never constructs a second sequencer or
body. The raw angular-velocity vector is applied without multiplying by
elapsed time, exactly as the named function does. A changed root commits the
entity, effect pose, and multipart collision shadow together. Zero omega does
not trigger a collision reflood, and a Hidden/nonresident object cannot have
its suspended shadow restored by this animation path.
- Animation hooks are captured after `UpdatePhysicsInternal` and before the
transition/manager tail. Semantic `AnimationDone` executes there, matching
`CPhysicsObj::process_hooks`. Other pose-dependent router sinks remain
deferred until final root/part/equipped-child pose publication; that known
residual is registered as TS-50 rather than claimed as exact ordering.
Static owners retain their semantic hook tail until final root, part, and
attached-child publication, matching `animate_static_object`'s later
`process_hooks` call. A residency change invalidates both the prepared pose
and that pending tail.
- Retail session dispatch and `CPhysics::UseTime` are single-threaded FIFO
operations. App therefore wraps complete packet and object-frame operations
in `RetailInboundEventDispatcher`: a callback-triggered packet queues behind
the current operation instead of recursively interleaving with its tail.
Position, State, Vector, and Movement keep separate authority versions;
Position/Vector/Movement additionally share only a velocity-write version.
Never replace these with one global body-mutation token: that incorrectly
discards independent accepted channels (for example State during Position).
The direct packet/frame path carries state into a cached static callback and
allocates nothing; only genuinely nested work receives a retained FIFO node.
- Projectile `BeginQuantum` is candidate-only. It restores the canonical begin
frame/dynamics before returning; `CompleteQuantum` may commit only while the
exact live record, projectile runtime, entity, spatial owner, and prediction
version remain current. Any accepted Position, Vector, or State invalidates
the candidate without rolling back the authoritative mutation.
- Animation-hook batches capture the pose owner's monotonic lifetime before
running semantic AnimationDone. Capture rechecks it before every completion,
and drain rechecks that lifetime and root pose before every routed hook,
because either AnimationDone or an earlier sink can delete the owner and
reuse the same local ID synchronously.
- Remote animation advances into one reusable DAT `Frame`; both components are
passed to `RemotePhysicsUpdater` and applied through the same
`PositionManager` frame.
- A body-backed animated object without a MovementManager now commits its
complete Frame through `LiveEntityOrdinaryPhysicsUpdater`: root translation
is admitted only while OnWalkable, orientation remains live, the resulting
candidate runs through Transition/SetPositionInternal, and the same record is
rebucketed without recreating resources. Callback deletion/replacement is
revalidated before every commit.
- The resolved body and `WorldEntity` root/full-cell frame are committed before
the canonical rebucket callback, matching SetPositionInternal's frame/contact
commit before its final shadow replacement. A loaded-to-pending rebucket (or
same-GUID replacement during its visibility callback) invalidates the caller
before shadow or manager-tail publication. Non-projectile shadow residency is
symmetric in `LiveEntityPresentationController`, including the pending-first
case whose initial false edge precedes collision registration; projectiles
retain their dedicated residency owner.
- Local projection and both authoritative remote UpdatePosition tails use the
same post-rebucket exact-owner gate. Remote collision refresh is forced by a
cell change or by a changed complete pose (translation or sign-invariant
quaternion), so offset/multipart Setup shapes rotate in place and a same-pose
cell crossing cannot retain an old flood seed.
- A retained projectile remains the same CPhysicsObj when Missile clears. With
no MovementManager it continues through the ordinary one-sphere transition
owner; with a MovementManager the owner transfers exactly once to remote
motion. Projection re-entry restores its retained collision shadow on the
visibility edge, before any later simulation quantum.
- Active interpolation preserves the target's complete quaternion and replaces
the PartArray Frame. The already-close `InterpolateTo` branch applies only
the target heading, matching `CPhysicsObj::set_heading`. `keep_heading` is
manager-wide, not a per-node flag; each near enqueue replaces it.
- `FrameOps.SetRotate` rejects zero, non-finite, and otherwise invalid
quaternion candidates by restoring the prior orientation.
- Spawn, teleport, correction, and outbound movement/position serialization
read the authoritative body quaternion directly; none round-trips through
the horizontal `Yaw` projection.
- `MotionTableDispatchSink` only dispatches motion-table commands. It no longer
reports turn callbacks.
- `RemoteMotion.ObservedOmega`, the player/NPC multiplication-order fork, and
`AnimationSequencer.SetCycle`'s synthetic `pi/2` turn omega are deleted.
- A controller deliberately constructed without a `PartArray` retains a
Humanoid-only fallback at the DAT-pinned `1.5` radians/second. A normal GUI
client does not enter that path.
## Conformance gates
- `MotionDeltaFrameTests`: exact `FrameOps.Combine` equivalence, scale affects
origin only, and temporal turn-then-move composition.
- `RetailObjectQuantumClockTests`: strict bounds, retained remainder,
MaxQuantum subdivision, HugeQuantum discard, `set_active` rebasing, and
Static-aware `prepare_to_enter_world` reset.
- `RetailObjectActivityGateTests` and `LiveEntityRuntimeTests`: the 96-unit
PartArray gate, parent/cell/Frozen suspension, loaded rebucket retention,
pending re-entry rebasing, body/clock Active synchronization, and GUID-reuse
clock isolation.
- `LiveEntityAnimationSchedulerTests`: the canonical live-object workset also
advances objects without a render animation, consumes their queued near
UpdatePosition correction, applies complete generic root
Frames, transfers a retained missile body exactly once when classification
clears, and stops catch-up immediately when a callback deletes, withdraws,
or replaces an incarnation.
- `RetailStaticAnimatingObjectSchedulerTests` and
`AnimationSequencerTests`: unconditional Setup DefaultAnimation installation,
DAT/live physics-Static workset membership, direct 30-fps playback,
short-residency-gap catch-up, canonical PartArray/body reuse, raw (not
elapsed-scaled) omega, synchronized multipart shadow rotation, zero-omega
no-op commits, callback-safe removal, and the retail greater-than-two-second
discard. `StaticLiveRootCommitter` coverage proves Hidden omega cannot
resurrect a suspended shadow.
- `RemoteInboundMotionDispatcherTests`: one animation-optional owner preserves
retail packet-head/style/type-0/sticky ordering with a null PartArray sink;
unknown movement types execute the head and never enter the case-0 funnel;
supersession during interrupt prevents every later packet side effect.
- `LiveEntityOrdinaryPhysicsUpdaterTests`: complete root Frames cross cells and
landblocks through Transition, while hook-side deletion prevents stale body,
shadow, pose, or spatial commits.
- `ProjectileControllerTests`: pending-cell hydration restores the same body's
Active/InWorld/shadow residency immediately and does not replay logical
creation; the destination cell is canonical before suspension and after
hydration, and authoritative Position/Vector/State between prediction halves
discards only the stale candidate.
- `RetailInboundEventDispatcherTests`, `LiveEntityPresentationControllerTests`,
`RemoteTeleportControllerTests`, and `EntityEffectPoseRegistryTests`: nested
packet arrival drains after the complete older tail, independent State and
Position both survive resolver callbacks, newer Position supersedes older
placement, Hidden-to-Visible recursion remains FIFO, and hook batches cannot
cross deletion/local-ID reuse during capture or drain. The FIFO's nonnested
state path is pinned at zero allocations after warmup.
- `PlayerMovementControllerTests`: full-frame local composition, large-frame
equivalence, hidden/airborne gates, hook cadence, and proof that
animation-backed turns are not integrated twice.
- `InterpolationManagerTests`: complete relative target orientation,
keep-heading, and already-close heading-only behavior.
- `RemotePhysicsUpdaterTests` and `LiveEntityPresentationControllerTests`:
translation uses the pre-turn orientation and the same frame then rotates the
body; cross-cell loaded-to-pending commits the destination root before the
callback boundary, suspends rather than re-adds collision, restores on
hydration, and isolates old shadows across callback GUID reuse. Direct
authoritative rebuckets and initial-pending materialization cover the same
symmetric collision-residency contract; same-position rotation and cell-only
changes refresh collision while unchanged/sign-flipped quaternion poses do
not reflood.
- `LiveEntityRuntimeTests`: update iteration and render-ID classification copy
only concrete spatial animation owners through reusable storage with zero
steady-state allocations.
- `MotionTableDispatchSinkTests`: Branch-4 turn preserves the locomotion cycle,
contributes literal DAT omega, and unwinds on stop.
- `RemoteChaseEndToEndHarnessTests`: turn, chase, re-arm, action completion, and
sticky following run through the complete Frame plus retail manager tail.
- `HumanoidMotionTableRootMotionTests`: installed retail walk displacement and
exact Humanoid `TurnRight` omega `-1.5` radians/second.
- `MovementManagerTests` and `PlayerMovementControllerTests`:
`PerformMovement` invokes `set_active(1)` before request validation, while a
Static physics object preserves retail's no-op activation behavior.
## Registered residual
The ordinary/static object worksets and their root/animation clocks are now
retail-shaped, but acdream's shared particle and PhysicsScript managers still
advance once per render frame after every owner has run. Retail advances those
tails inside each admitted object update (and inside
`animate_static_object`). This timing difference is deliberately registered as
TS-51; it is not part of the R6 conformance claim.
Final automated gate (2026-07-20): three independent retail-conformance,
architecture/integration, and adversarial re-reviews reported no actionable
finding after correction; Release build completed with zero warnings; the full
solution passed 6,446 tests with 5 intentional skips.