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:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`) |
|
||||
|
|
|
|||
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal file
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue