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

@ -207,16 +207,22 @@ src/
Physics/
ProjectileController.cs -> live-record projectile orchestration/corrections
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
LiveEntityOrdinaryPhysicsUpdater.cs -> manager-less body Transition commits
RemoteInboundMotionDispatcher.cs -> animation-optional retail UM funnel
RemoteTeleportController.cs -> incarnation-scoped loaded/pending placement owner
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
World/
LiveEntityRuntime.cs -> canonical identity/state/body/projectile/spatial ownership
RetailInboundEventDispatcher.cs -> update-thread packet/frame FIFO barrier
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
RetailLiveFrameCoordinator.cs -> object/physics-before-network frame barrier
Rendering/
GameWindow.cs -> still owns too much runtime wiring
LiveEntityAnimationScheduler.cs -> canonical ordinary live-object workset
RetailStaticAnimatingObjectScheduler.cs -> retail static-animation workset
StaticLiveRootCommitter.cs -> static root pose/collision commit boundary
TerrainModernRenderer.cs -> mandatory bindless+MDI terrain path
Wb/WbDrawDispatcher.cs -> ordinary live/static entity draw dispatch
Wb/EnvCellRenderer.cs -> indoor cell-shell draw path
@ -285,6 +291,67 @@ What exists and is active:
owns the frame;
delete, generation replacement, pickup/parent leave-world, and session reset
use `LiveEntityRuntime`'s normal lifecycle and never create a second GUID map.
- `LiveEntityAnimationScheduler` is the one update-thread walk over spatial
live roots. It admits each incarnation's `RetailObjectQuantumClock`, advances
PartArray state, then selects exactly one movement owner: remote motion, the
retained projectile body, or `LiveEntityOrdinaryPhysicsUpdater` for a
manager-less canonical body. Every hook boundary revalidates record,
component, and object-clock epoch before transition, manager-tail, pose, or
rebucket commits. `RetailStaticAnimatingObjectScheduler` remains a separate
workset, matching `CPhysics::UseTime`: Setup DefaultAnimation is installed at
PartArray construction, while DAT and live PhysicsState-Static owners advance
whole elapsed intervals through `animate_static_object` semantics. A live
static owner shares the record's canonical PartArray and PhysicsBody; raw
static omega commits the root, effect pose, and collision shadow together,
while zero omega does not reflood collision and Hidden never restores it.
The typed animation view copies the runtime's concrete spatial dictionary
through reusable storage, so update and render classification allocate
nothing and scale with resident owners rather than retained KnownObjects.
- `RemoteInboundMotionDispatcher` is the single remote
`MovementManager::unpack_movement` owner. Animated and animation-less live
objects differ only by the presence of a PartArray dispatch sink; packet
head interrupt/style routing, MoveTo cases, case-0 wholesale state, sticky,
and long-jump ordering cannot drift between two `GameWindow` branches.
- Ordinary remote/manager-less Transition commits publish the resolved body,
`WorldEntity` root, and full cell before invoking the canonical rebucket
writer. That writer is a re-entrant lifetime boundary: the caller immediately
revalidates the exact record/runtime and publishes no collision shadow or
manager tail if the destination became pending or the GUID was replaced.
`LiveEntityPresentationController` owns the symmetric non-projectile
collision-residency edge: loaded-to-pending suspends the retained shadow,
hydration restores it immediately, and `OnLiveEntityReady` reconciles an
object that materialized pending before its collision registration existed.
Direct local-player and authoritative remote publication use the same
ordering: commit root, rebucket, then prove exact-record spatial residency
before touching collision. Remote shadows are pose/cell-gated, including
complete sign-invariant orientation for offset/multipart Setup shapes and a
forced refresh on cell-only transitions. Projectile residency remains
exclusively in `ProjectileController`.
- `RetailInboundEventDispatcher` wraps every live-object session callback and
each complete live-object frame phase. Synchronous App observers can enqueue
another packet, but that packet cannot interleave halfway through the older
packet's collision, rebucket, shadow, pose, hook, or manager tail. The live
record retains independent Position, State, Vector, and Movement authority
versions plus a narrow shared velocity version for the three packet families
that can install velocity. This mirrors retail's single update-thread FIFO
without incorrectly making an accepted State cancel an accepted Position.
Its direct frame/packet path accepts state plus a cached static callback and
performs zero allocation; only real nested reentrancy materializes a queued
heterogeneous operation.
- Projectile prediction is a two-phase transaction around retail's
`process_hooks` slot. `BeginQuantum` holds a candidate while leaving the
canonical body at its begin frame; `CompleteQuantum` commits only if the
exact record/runtime and prediction version still match. Position, Vector,
and State corrections therefore win if accepted between the two halves.
Teleport placement uses the same exact-record and per-channel authority
checks, so a newer Position supersedes an older resolver while an independent
State is preserved alongside the completed placement.
- `EntityEffectPoseRegistry` assigns a monotonic lifetime to every local-ID
incarnation. `AnimationHookFrameQueue` snapshots it before semantic
`AnimationDone` callbacks and revalidates it before every semantic completion
and every routed hook. A delete/local-ID reuse during capture or during an
earlier hook can never advance the displaced sequencer or send the old
owner's remaining sound, particle, or light hooks to its replacement.
- `RemoteTeleportController` owns the placement half of a fresh remote
teleport after `RemoteTeleportHook` has torn down movement/target state. It
collision-seats loaded destinations through `RemoteTeleportPlacement`; an

View file

@ -165,6 +165,9 @@ src/AcDream.App/
├── Rendering/
│ ├── GameWindow.cs # thin: GL/window lifecycle + delegates per-frame to RenderFrameOrchestrator
│ ├── RenderFrameOrchestrator.cs # per-frame draw order (sky → terrain → opaque → trans → particles → debug → UI)
│ ├── LiveEntityAnimationScheduler.cs # shipped: ordinary live-object update workset
│ ├── RetailStaticAnimatingObjectScheduler.cs # shipped: separate static-animation workset
│ ├── StaticLiveRootCommitter.cs # live static root → pose + collision boundary
│ ├── TerrainModernRenderer.cs # (already exists)
│ ├── TextureCache.cs # (already exists)
│ ├── ParticleRenderer.cs # (already exists)
@ -176,12 +179,16 @@ src/AcDream.App/
├── Physics/
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
│ ├── RemotePhysicsUpdater.cs # ordinary/Hidden remote narrow-tick integration
│ ├── LiveEntityOrdinaryPhysicsUpdater.cs # manager-less canonical body Transition path
│ ├── LiveEntityShadowPublisher.cs # authoritative exact-owner/residency collision gate
│ ├── RemoteInboundMotionDispatcher.cs # shared animated/headless UpdateMotion funnel
│ ├── RemoteTeleportController.cs # loaded/pending teleport placement ownership
│ ├── RemoteTeleportHook.cs # ordered retail teleport teardown actions
│ └── RemoteTeleportPlacement.cs # collision-seated SetPosition transition commit
├── World/
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
│ ├── RetailInboundEventDispatcher.cs # update-thread packet/frame FIFO barrier
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
@ -239,6 +246,64 @@ allocators fail fast instead of wrapping into another landblock's namespace.
All other non-Parent packet
families still need the future general queue tracked by divergence AD-32.
The per-frame object body is no longer an animation-dictionary loop inside
`GameWindow`. `LiveEntityAnimationScheduler` snapshots canonical spatial root
records and advances the incarnation-stable object clock, PartArray, hooks,
one selected movement owner, and manager tail in retail order. Manager-less
bodies delegate their candidate/Transition/cell/shadow commit to
`LiveEntityOrdinaryPhysicsUpdater`; retained projectile bodies and remote
MovementManagers remain mutually exclusive movement owners. Static animation
is deliberately separate: `RetailStaticAnimatingObjectScheduler` owns the
`CPhysics::static_animating_objects` workset for DAT and live PhysicsState-
Static owners with Setup DefaultAnimation. Both schedulers are wired by
`GameWindow`, but neither owns GUID identity or logical resources.
The typed animation view fills its reusable snapshot and render-ID set from
`LiveEntityRuntime`'s concrete spatial dictionary, avoiding interface-enumerator
boxing on both update and render hot paths.
`RemoteInboundMotionDispatcher` similarly keeps UpdateMotion protocol behavior
outside `GameWindow`: GameWindow resolves the canonical record/body and the
optional PartArray sink, while one dispatcher owns retail's interrupt, style,
MoveTo/type-0, sticky, and standing-long-jump order. Static root projection is
bounded by `StaticLiveRootCommitter`, which synchronizes changed roots to
effects and collision without rebuilding zero-omega shadows or resurrecting a
Hidden/withdrawn registration.
Synchronous network and lifecycle callbacks are bounded by
`RetailInboundEventDispatcher`. It owns no wire state and no identity; it only
serializes nested live-object operations until the current packet or full
object-frame tail completes. State-bearing direct dispatch is allocation-free;
only a genuinely nested operation allocates its retained queue wrapper.
`LiveEntityRecord` then supplies exact-incarnation
and per-channel authority versions at callback boundaries. Position, State,
Vector, and Movement remain independent, while a separate velocity version
invalidates only an older operation that would overwrite a newer velocity
installed by Position, Vector, or Movement. This prevents re-entrant App
observers from creating call-stack ordering that retail's update-thread packet
FIFO cannot produce.
Pose-dependent hook deferral is similarly incarnation-scoped rather than GUID-
or local-ID-scoped. `EntityEffectPoseRegistry` publishes a monotonic pose-owner
lifetime, and `AnimationHookFrameQueue` captures it before semantic callbacks
and rechecks it before each semantic AnimationDone and each routed hook. Static animation retains `process_hooks`
until its root, live parts, and children are published; withdrawal invalidates
both its prepared pose and pending hook tail.
Resolved ordinary motion commits its body/root/contact state before the
canonical full-cell setter enters `LiveEntityRuntime.RebucketLiveEntity`.
Because that setter may synchronously move the projection to pending or replace
the GUID, both `RemotePhysicsUpdater` and `LiveEntityOrdinaryPhysicsUpdater`
revalidate the exact incarnation before collision or manager-tail publication.
Collision residency itself is projection-owned, not updater-owned:
`LiveEntityPresentationController` suspends retained non-projectile shadows on
every unavailable-projection edge, restores them on hydration, and reconciles
the pending-first case at `OnLiveEntityReady` after collision registration.
Local projection and both authoritative remote UpdatePosition tails commit the
complete root before rebucketing, then publish collision only through an
exact-record/spatial-residency gate. Remote reflood tracks translation,
sign-invariant complete orientation, and cell changes so an in-place turn or a
same-pose EnvCell crossing cannot leave offset Setup shapes stale.
The projectile controller retains its separate body/InWorld/shadow edge owner.
Remote teleport placement is bounded in `Physics/RemoteTeleportController`,
not `GameWindow`: it retains at most one pending request per materialized
incarnation, scopes it by the live generation and accepted PositionSequence,

View file

@ -61,7 +61,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 2. Adaptation (AD) — 38 rows (AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover)
## 2. Adaptation (AD) — 37 rows (AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover)
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -105,7 +105,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 3. Documented approximation (AP) — 89 active rows
## 3. Documented approximation (AP) — 85 active rows
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
@ -176,9 +176,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-73 | **Character raises mutate optimistically, contrary to retail's server-authoritative flow** — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, `CharacterSheetProvider.ApplyLocalRaise` immediately bumps ranks and debits XP/credits. Named retail permits one request in flight, ghosts the clicked button, and waits for an authoritative quality-change element message before changing displayed state (**#199**). | `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` | ACE usually accepts client-affordable raises, so its later property echoes conceal the incorrect prediction; Wave 8 removes local mutation and owns one awaiting request | A rejected/reordered raise can display invented state until a later full refresh, and repeated clicks can create multiple speculative spends | `gmAttributeUI`/`gmSkillUI` raise and quality-change paths, pinned in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
---
| AP-75 | **NARROWED 2026-07-17 — adapter-boundary `adjust_motion` + turn-omega synthesis only.** The locomotion-velocity half is RETIRED: CSequence now retains literal `MotionData.Velocity × speed`, and remote translation consumes the complete Frame emitted by `CSequence::update` before interpolation. Remaining: `SetCycle` (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in `CMotionInterp`; GameWindow's local-player path can still pass raw ids), and (b) synthesizes ±π/2×speed omega for dat-silent TurnRight/TurnLeft after dispatch | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap + turn-omega tail); retired velocity consumer in `RemotePhysicsUpdater`/`RemoteMotionCombiner` | (a) preserves unadjusted local callers until they all enter through MotionInterpreter; (b) preserves remote rotation until the literal CSequence orientation delta replaces the ObservedOmega side channel (AP-76) | A DAT with authored turn omega different from π/2 is overridden only when the table is silent; boundary remapping can become a double-adjust if a future caller already normalizes a raw left/back command but still passes its original id | `CMotionInterp::adjust_motion` @305343; `add_motion` 0x005224B0; `CPhysicsObj::UpdatePositionInternal` 0x00512C30; retire with the remaining R6 rotation/caller unification |
| AP-76 | **Remote rotation from the ObservedOmega side-channel**: the R2-Q5 sink callbacks (`MotionTableDispatchSink.TurnApplied/TurnStopped`) seed `RemoteMotion.ObservedOmega = (0,0,-(pi/2)*signedTurnSpeed)` on wire turns and zero it on turn-stops; GameWindow's per-tick step applies ObservedOmega (preferring it over sequence omega) to the remote body's orientation. Retail rotates the body from the SEQUENCE omega inside the per-tick apply_physics chain (CSequence::apply_physics -> CPhysicsObj omega integration) (carried verbatim from the deleted RemoteMotionSink; H17) | `src/AcDream.App/Rendering/GameWindow.cs` (sink callbacks in OnLiveMotionUpdated + the omegaToApply step in TickAnimations) | Same angular rate retail derives (pi/2 rad/s x turn speed); starts rotation the same tick as the wire turn without waiting for R6's per-tick order; UpdatePosition orientation snaps bound any drift | If the dat's turn modifier omega differs from the pi/2 constant, remote rotation rate diverges until an orientation snap; double-application risk if R6 lands apply_physics-driven rotation without deleting this seam | retire in R6 (retail per-tick order: apply_physics drives remote rotation) |
| AP-77 | **NARROWED 2026-07-17 — null-sink/headless `apply_current_movement` fallback only.** When `MotionInterpreter.DefaultSink` is absent, `ApplyCurrentMovementInterpreted` writes grounded body velocity directly via `get_state_velocity`/`set_local_velocity` instead of dispatching through the animation-table backend. Every production animated player and remote binds `MotionTableDispatchSink`; the local controller also consumes literal sequencer root displacement, so this tail is not the live humanoid locomotion owner | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`) | Keeps MotionInterpreter usable in isolated physics tests and for an entity with no animation runtime; production animation owners take the retail sink path | A future production entity instantiated without a DefaultSink silently falls back to command-derived body velocity and can foot-slide or drift from its animation | `CMotionInterp::apply_interpreted_movement` 0x00528600; retire when animation-less production objects have an explicit motion owner |
| AP-75 | **NARROWED 2026-07-19 — adapter-boundary `adjust_motion` only.** `SetCycle` remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in `CMotionInterp`; GameWindow's local-player adapter can still pass raw ids directly | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap) | Preserves raw local callers until every caller enters through `MotionInterpreter`; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | `CMotionInterp::adjust_motion` @305343; retire with the remaining local caller unification |
| AP-77 | **NARROWED 2026-07-19 — animation-less/headless movement fallback only.** When `MotionInterpreter.DefaultSink` or the local PartArray callback is absent, acdream writes grounded command-derived body velocity and applies the DAT-pinned Humanoid `TurnRight` rate (1.5 radians/second) directly to the body Frame. Production animated players/remotes bind `MotionTableDispatchSink` plus CSequence and instead consume the complete DAT-authored root Frame; that path preserves airborne orientation while suppressing only origin exactly like retail | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`); `src/AcDream.App/Input/PlayerMovementController.cs` (no-PartArray object-quantum fallback) | Keeps isolated/headless physics tests and a deliberately animation-less entity controllable without fabricating a PartArray | A future production entity missing its animation binding uses Humanoid-only yaw/velocity, can foot-slide, and can rotate through an airborne quantum differently from a real creature's DAT Frame | `CMotionInterp::apply_interpreted_movement` 0x00528600; `CPhysicsObj::UpdatePositionInternal` 0x00512C30; retire when animation-less production objects have an explicit motion owner |
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables. The adaptation is now structurally limited to replacing Ready/Walk/Run-family states, so authoritative actions/substates (especially Dead) always win. | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`, `CanApplyVelocityCycle`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
| AP-81 | **Remote-DR gravity toggled via the Gravity STATE bit**: the jump handler sets `Body.State \|= Gravity` at VectorUpdate and both landing blocks clear it after `HitGround()`; retail keeps GRAVITY set for the object's whole life and gates gravity ACCELERATION on the Contact transient (`calc_acceleration`) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so `Motion.HitGround()`'s verbatim `state&0x400` gate runs BEFORE the clear) | `src/AcDream.App/Rendering/GameWindow.cs` (VectorUpdate jump handler + the two landing blocks) | The DR tick integrates gravity only for airborne remotes; the flag dance delivers exactly that without porting the full contact-gated `calc_acceleration` chain; the #161 ordering fix keeps the retail HitGround contract satisfied | Any NEW call into `Motion.HitGround`/`LeaveGround` placed after the clear silently no-ops on the gravity gate (the #161 leg-2 class); grounded remotes carry a non-retail state word (probes comparing state bits vs retail mislead) | `CPhysicsObj::calc_acceleration` (contact-gated); `set_on_walkable` 0x00511310; retire in R6 (contact-gated accel + persistent GRAVITY) |
| AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = (speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) |
@ -186,9 +185,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-84 | **BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose** (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose (the closed pose for doors — `GameWindow.MotionTableDefaultPose`); retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | `src/AcDream.App/Rendering/GameWindow.cs` (`MotionTableDefaultPose` + the RegisterServerEntityCollision override); `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`partPoseOverride`) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | `CPhysicsPart` live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities |
| AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
| AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
| AP-86 | **Remote SHADOW-follows-resolved via a movement-gated per-tick re-flood** (remote-creature de-overlap #184, 2026-07-07): a moving NPC remote's collision shadow is re-registered at its RESOLVED body position (`SyncRemoteShadowToBody``ShadowObjects.UpdatePosition`) so neighbours + the player de-overlap / collide against where the monster actually is (== where it renders), and the de-overlap PERSISTS. This is retail-faithful in EFFECT (retail re-registers a moved object's shadow every accepted transition step, `SetPositionInternal``remove/add_shadows_to_cells` 0x00515330), but the IMPLEMENTATION differs: acdream runs the FULL `RegisterMultiPart` cell-flood, gated on `|BodyLastShadowSyncPos| > 1 cm`, rather than an in-place sphere translate with cell-relink-only-on-change; and the per-UP raw-worldPos shadow sync is RETIRED for EVERY remote (Slice 2b, 2026-07-08 — was players-only through Slice 1): every remote's shadow (player + NPC) is written ONLY by this per-tick loop + the UP-branch tail, both to the RESOLVED body, since Slice 2b collapsed the player/NPC fork so grounded PLAYER remotes also run the sweep + shadow-follows-resolved. Proven: `RemoteDeOverlapMechanismTests` (with-sync 0.86 m stable vs without-sync <0.40 m; real-interp loop absorbs the stall-blip incl. the `ConvergingPlayers_RealInterpLoop` player-config pair added in Slice 2b) | `src/AcDream.App/Rendering/GameWindow.cs` (`RemotePhysicsUpdater.SyncRemoteShadowToBody` + the DR-tick movement gate; the NPC UP-branch tail sync + the player UP-branch tail sync; the RETIRED raw-pos sync site a comment where the players-only `:5669` block used to be); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The movement gate is exact at the de-overlap equilibrium (the resolved position genuinely stops moving there, so no needed sync is skipped) and the flood result is identical to retail's per-step re-register; the cost is the only divergence | A dense town of many animated remotes re-floods per moving creature per tick (Gen0 churn the MP-track FPS class); a still crowd is gated out. Optimize with an in-place shadow-move if profiling shows it. `rm.CellId==0` on a partial resolve keeps the prior cell (same pre-existing exposure as `:5669`) only a landblock-crossing on that tick misplaces the shadow | `SetPositionInternal` 0x00515bd0 `change_cell`/`add_shadows_to_cells` 0x00515330 (registers shadow from resolved `m_position`) |
| AP-86 | **Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood** (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`; `src/AcDream.App/Physics/LiveEntityShadowPublisher.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (local projection + authoritative UP tails); `src/AcDream.App/World/LiveEntityPresentationController.cs` (ordinary projection residency); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | `CPhysicsObj::SetPositionInternal(CTransition const*)` 0x00515330 → `change_cell`, then `remove_shadows_from_cells`/`add_shadows_to_cells` after the resolved frame/contact commit |
| AP-87 | **NPC MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the 96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions `|Body.Position worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body garbage resolved pos the reverted attempt's INVISIBLE monster. `firstUp` (`LastServerPosTime<=0`) is a belt hint only the 4 m guard is the load-bearing backstop | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC MoveOrTeleport routing, `BodySnapThresholdNpc`/`willBeDrTickedNpc`) | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap 96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) |
| AP-88 | **Remote omega is reconstructed with a player/NPC fork retail does not have** (remote-creature de-overlap #184 Slice 2b, 2026-07-08): retail's `UpdateObjectInternal` applies ONE angular-velocity integration to every object; acdream reconstructs remote omega from the wire and keeps a player/NPC split inherited from the two former DR paths — a grounded PLAYER remote applies `ObservedOmega ∥ seqOmega` (falls back to the sequencer's synthesised cycle omega when the wire-TurnCommand-derived `ObservedOmega` is 0 — the "circling player sends RunForward+TurnLeft on ONE UM" case) in the WORLD frame (pre-multiply, `Quaternion.Concatenate`); an NPC or an AIRBORNE body applies `ObservedOmega`-only in the BODY frame (post-multiply, `Quaternion.Multiply`). Both feed the same downstream integrate; `calc_acceleration` zeroes `Body.Omega` for grounded bodies so `UpdatePhysicsInternal` never double-integrates | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (`Tick`, Step 2 omega fork) | For an UPRIGHT body (the only remote pose — creatures/players never pitch or roll; the wire orientation is yaw-only) rotating about world-Z (the only turn axis) the pre- and post-multiply orders COMMUTE and both branches reduce to the same yaw increment; the seqOmega fallback only adds rotation a circling player genuinely has, and applying it to NPCs would spuriously add their baked cycle omega — so the fork is behaviourally faithful for every reachable pose (it is what the pre-2b Path A / Path B already did, now explicit in one method). ALSO: the merge applies omega BEFORE `ComputeOffset` for everyone (Path B's order), whereas pre-2b Path A applied a grounded PLAYER's omega AFTER its compose — so the `ori` used to rotate CSequence's literal root-motion delta when the interpolation queue is empty/head-reached is yaw-advanced by one tick (~ω·dt ≈ 2° at 2.24 rad/s, non-accumulating, zero while catch-up is active). Keeping Path B's order leaves the shipped/gate-passed NPC omega untouched and only nudges a turning grounded player's fallback direction ~2° in rare queue-empty frames — cosmetically negligible; adopting Path A's order instead would have perturbed NPCs by the same amount | If a remote ever acquires a non-yaw orientation (pitch/roll — a future flying mount / ragdoll) the two multiplication orders diverge and a player would rotate differently from an NPC at the same omega; collapse to one order + a shared fallback policy then | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (single apply_physics omega integration, no object-class fork); `apply_interpreted_movement` 0x00528600 (retail's server-driven omega source acdream reconstructs) |
| AP-89 | **TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel** (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's `CPhysicsPart::SetTranslucency` (0x0050e670) → `CMaterial::SetTranslucencySimple` (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (`Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 translucency`) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's `mesh_modern.frag` has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled `color.a` (`FragColor = vec4(rgb, color.a * vOpacityMultiplier)`) | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (final `FragColor` line); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`ClassifyBatches` `opacityMultiplier` param, `InstanceGroup.Opacities`); `src/AcDream.Core/Rendering/TranslucencyFadeManager.cs` | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since `color.a * 1.0 == color.a` and the fade multiplier alone then drives the ramp exactly as `1 translucency` would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | `CPhysicsPart::SetTranslucency` 0x0050e670; `CMaterial::SetTranslucencySimple` 0x005396f0 (`alpha = 1 translucency`, applied to all 4 D3D9 material alpha channels) |
| AP-90 | **Radar fellowship/allegiance relationship state is modeled but not yet delivered at runtime.** `RetailRadar.GetBlipShape` and `RadarBlipColors.For` implement retail's leader/member/allegiance precedence, and `RadarSnapshotProvider` exposes a `relationshipFor(guid)` seam, but acdream does not yet maintain live fellowship membership and its `AllegianceTree` is not wired into GameWindow. PK/PKLite relationship shapes do work from PWD flags. | `src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs`; `src/AcDream.Core/Ui/RetailRadar.cs`; `src/AcDream.Core/Ui/RadarBlipColors.cs` | Preserve the exact model/seam now and avoid inventing membership from names or chat; connect it when the social game-event state is ported | Fellowship members render their ordinary player color/shape instead of bright-green leader/member triangles; allegiance members render an ordinary plus instead of a hollow box | `gmRadarUI::GetBlipColor` 0x004D76F0; `gmRadarUI::GetBlipShape` 0x004D7B60 |
| AP-92 | Paperdoll renders its creature through a private FBO/RTT and blits it into `UiViewport`; retail renders `CreatureMode` directly in the UI viewport/in-cell presentation path | `src/AcDream.App/Rendering/PaperdollViewportRenderer.cs`; `src/AcDream.App/UI/UiViewport.cs` | GL RTT is the modern backend equivalent and the accepted pixels/camera/pose match retail; state is sealed with `GLStateScope` | FBO origin, alpha, lighting, or state isolation can diverge from direct viewport rendering | `gmPaperDollUI::PostInit @ 0x004A5360`; `UIElement_Viewport::SetCamera`; retail `CreatureMode::Render` |
@ -216,7 +214,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus `ready -> NonCombat`, or post-wield `NonCombat -> ready -> NonCombat`, causes one normal `ChangeCombatMode` request from the trailing notice. Explicit user combat input cancels settlement. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during primary-weapon replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later explicit combat request, replacement, or logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` |
## 4. Temporary stopgap (TS) — 32 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed 2026-07-07 — NPC UP unified onto the interp queue, gate retained for orientation)
## 4. Temporary stopgap (TS) — 34 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -249,12 +247,14 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 |
| TS-38 | `MotionInterpreter.Initted` defaults to `true` in both constructors, not retail's `false` — retail's `CMotionInterp` is never observed pre-`enter_default_state` (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`Initted` property + both constructors) | Defaulting `true` is the C# equivalent of "the constructor already did what `enter_default_state` would have done to this flag" — `EnterDefaultState()` remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, `LeaveGround` tail) when a caller wants them | None observed: no code path needs `apply_current_movement`/`ReportExhaustion` to no-op before an explicit `EnterDefaultState()` call, since nothing constructs a `MotionInterpreter` and defers initialization today. If a future caller DOES need staged construction (build now, `EnterDefaultState()` later), it must explicitly set `Initted = false` first | `CMotionInterp::enter_default_state` 0x00528c80 @306124 sets `initted = 1`; retire if/when construction is staged through `EnterDefaultState()` uniformly |
| ~~TS-41~~ | **RETIRED 2026-07-07 (remote-creature de-overlap #184)** — the SERVERVEL synth-velocity body-drive (`Body.Velocity = ServerVelocity` / `get_state_velocity()` leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (`RemoteMotionCombiner.ComputeOffset``InterpolationManager::adjust_offset` toward the MoveOrTeleport-queued server waypoint) and `MovementManager::UseTime` (`TickRemoteMoveTo`) runs UNCONDITIONALLY per tick — the retail `UpdateObjectInternal` shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (`ApplyServerControlledVelocityCycle(Zero)` on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by **AP-80**; it no longer drives the body. | `src/AcDream.App/Rendering/GameWindow.cs` (`TickAnimations` grounded NPC branch) | — | — | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (`MovementManager::UseTime` @0x00515998, unconditional); `MoveOrTeleport` 0x00516330; `InterpolationManager::adjust_offset` 0x00555d30 |
| TS-44 | NPC UpdatePosition reconciliation (position + orientation) is SUPPRESSED while the entity is stuck (`PositionManager.GetStickyObjectId() != 0`). **#184 (2026-07-07) UNIFIED the NPC path onto the interp queue** — the retire-condition mechanism is now ported: the grounded UP routes through `CPhysicsObj::MoveOrTeleport` (Enqueue) and the per-tick catch-up SEEDS the `PositionManager::adjust_offset` chain where `StickyManager::adjust_offset` OVERWRITES it while armed (0x00555190 / 0x00555430 assigns m_fOrigin), exactly like the player-remote branch — so POSITION is now handled retail-faithfully by the compose-overwrite whether the gate is on or off. The `snapSuppressedByStick` gate is RETAINED for two residuals: (a) it suppresses the during-stick **Enqueue** so a stale server waypoint never enters the queue, and (b) it gates the **orientation** hard-snap (still outside the interp chain — retail's sticky owns facing). Bookkeeping (`LastServerPos/Time`, cell) still records; server truth reasserts on the first UP after unstick | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC section, `snapSuppressedByStick` gate) | Retail's position mechanism (sticky-overwrites-interp) is now REACHABLE for NPCs (#184 gave them the interp-queue architecture); the gate remains only for orientation + during-stick Enqueue cleanliness | A stick that stays armed while ACE moves the monster far (shouldn't happen — sticks follow the target) keeps a stale orientation until unstick+next UP; bounded by the 1 s lease | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330 (NPC UP routing, #184); retire the orientation residual in R6 |
| TS-44 | NPC UpdatePosition **enqueue is suppressed while StickyManager is armed** (`PositionManager.GetStickyObjectId() != 0`). Position and complete orientation otherwise share the ported `InterpolateTo → Position::subtract2 → PositionManager::adjust_offset` Frame, so the former orientation hard-snap residual is retired. Retail would still enqueue the server Position and let Sticky overwrite that Frame each tick; acdream retains the gate so no queued waypoint survives the stick | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC `snapSuppressedByStick` gate) | Avoids replaying an old ACE waypoint immediately after a stick lease ends; all live during-stick pose ownership is now otherwise retail-shaped | After unstick the body waits for the next UP instead of consuming the latest waypoint already in the queue; at low packet cadence this can pause correction for one update interval | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330; retire by allowing enqueue while Sticky overwrites the shared complete Frame |
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.App/Input/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep now `GetSetupCylinder`-fed with a shapeless→human fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal rε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-84` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
| TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` |
| TS-49 | Hidden-object availability is bridged through `TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld)` because acdream has not ported retail's DetectionManager. Retail `CObjCell::hide_object` sends `LeftDetection` to detection voyeurs; acdream instead uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. | `src/AcDream.App/Rendering/EntityPhysicsHost.cs` (`NotifyHidden`); `src/AcDream.Core/Physics/Motion/TargetManager.cs` (`NotifyVoyeurOfEventAndClear`) | The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no `LeftDetection`; only movement/sticky target consumers observe the equivalent availability loss. | `CObjCell::hide_object @ 0x0052BE30`; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through `LeftDetection` |
| TS-50 | `AnimationDone` executes semantically at each owner's retail `CPhysicsObj::process_hooks` boundary, but all other animation hooks are retained in `AnimationHookFrameQueue` until final root/part/equipped-child pose publication. Retail executes the complete hook stream before transition and the Target/Movement/PartArray/Position manager tail because its current CPartArray pose already exists in-place. Static owners correctly reach `process_hooks` only after their root, parts, and children are current. | `src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs`; `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs`; frame drain in `src/AcDream.App/Rendering/GameWindow.cs` | The modern renderer publishes immutable effect-pose snapshots after all root/child composition; deferred visual sinks avoid attaching particles/lights/audio to the previous pose. Semantic `AnimationDone` is split out and exact, so motion completion and manager behavior are not delayed. Pose-owner lifetime tokens prevent deferred hooks from crossing delete/local-ID reuse. | A non-AnimationDone hook with same-quantum semantic consequences (notably `CallPES`, default-script chaining, audio/particle creation relative to a transition) runs later than retail and can observe post-tail state or start one render frame late. | `CPhysicsObj::process_hooks @ 0x00511550`; `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by publishing the current per-object/child pose before hook routing or splitting semantic and presentation sinks without changing authored hook order |
| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each object's ParticleManager and ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` also advances that static owner's managers using its whole admitted elapsed interval. | `src/AcDream.App/Rendering/GameWindow.cs` (`AdvanceLiveObjectRuntime` final `_particleSystem.Tick(dt)` / `_scriptRunner.Tick(...)`) | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and manager order faithful without pretending the shared tails have per-owner timing. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static default scripts/particles use render elapsed rather than `animate_static_object` elapsed/discard timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum |
---

View file

@ -872,9 +872,52 @@ diagnostic scaffolding, not yet the final collision system.
183-case/funnel/moveto/chase/sticky suites unmodified. Arc close-out:
`docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md`. Open follow-ons
carried: #167 (ConstraintManager arming). TS-42's per-tick completion/
manager order retired in the first R6 cutover on 2026-07-19; complete-frame
plumbing, PositionManager interpolation, quantum ownership, and side-channel
deletion remain R6 scope.
manager order retired in the first R6 cutover on 2026-07-19. The second R6
cutover now carries complete CSequence Frames (translation + DAT omega),
one incarnation-stable live-object quantum clock across local, remote,
Hidden, and projectile components, complete interpolation orientation, and
no ObservedOmega/synthetic-turn side channel. It also ports the 96-unit
PartArray Active gate plus immediate-active `prepare_to_enter_world` rebasing
and preserves live identity across loaded/pending rebucketing. Semantic
AnimationDone is in retail's pre-transition manager slot; TS-50 explicitly
carries the remaining pose-dependent hook-router delivery delay. The
Setup/state-driven `static_animating_objects` workset is now separate as
retail requires for both DAT and live physics-Static owners, including
unconditional PartArray default installation, direct 30-fps
DefaultAnimation, short residency-gap catch-up, and the greater-than-two-
second discard. Live static entries reuse the canonical record PartArray and
body; `animate_static_object` applies its raw omega without elapsed scaling
and commits changed render/effect/collision roots together without zero-omega
refloods or Hidden-shadow resurrection.
No-animation live remotes remain in the canonical ordinary-object workset,
and one animation-optional inbound dispatcher now owns their same retail
UpdateMotion funnel and near-correction consumption. `PerformMovement`
reactivates its owner before validation. A shared update-thread inbound FIFO
now prevents synchronous App callbacks from interleaving packets or frame
tails, while exact-record Position/State/Vector/Movement authority versions
preserve retail's independent timestamp channels and a narrow velocity token
resolves their one overlapping write. Projectile Begin/Complete prediction,
teleport placement, landing callbacks, Hidden transitions, and deferred
animation hooks all revalidate those exact ownership boundaries. Pose-owner
lifetime versions are captured before AnimationDone and checked before each
semantic completion and routed hook, closing delete/local-ID reuse during
both capture and drain. Direct frame/packet FIFO dispatch and spatial
animation update/render traversal are allocation-free after warmup; only
real nested work allocates a retained queue node. TS-51 records
the remaining shared particle/PhysicsScript tail timing. The ordinary
live-object scheduler and generic body transition path now live outside
GameWindow; body-backed animations, retained post-Missile bodies, visibility-
edge clock/shadow residency (including initial pending materialization),
callback-safe cross-cell rebucketing, and teardown all share the canonical
live record. Local and authoritative remote collision publication now occurs
only after exact-owner spatial revalidation; remote shadows refresh for
translation, complete orientation, or cell change. Folding
InterpolationManager into the owned
PositionManager facade remains the final R6 ownership cleanup.
Final 2026-07-20 gate: three independent retail/architecture/adversarial
re-reviews are clean; Release build is warning-free; the repository suite is
6,446 passed / 5 skipped. A connected locomotion/collision/projectile/
teleport visual and performance rebaseline remains the next gate.
- **L.2g — Inbound motion interpretation (remote-entity CMotionInterp funnel).**
ACTIVE 2026-07-02. Port retail's inbound motion pipeline verbatim for ALL
remote entities (players, NPCs, monsters — one funnel, user-approved):

View file

@ -10,10 +10,15 @@ favorite spell bar, connected casting/enchantment effects, and portal-space
presentation have passed their single-client visual gates. The corrective bounded
render/resource-lifetime integration passed its seven-destination connected
stress gate without a crash or cumulative collapse. The remaining M3 visual
requirement is the final two-client portal-out/materialize observer gate.
requirement is the final two-client portal-out/materialize observer gate. The
new R6 object-update cutover also requires one connected locomotion/collision/
projectile/teleport visual and performance rebaseline before that observer gate.
Carried:
#145-residual, #116 slide-response, the remaining R6 object-pipeline cutover
(TS-42 manager order retired 2026-07-19), and Track MP0.
#145-residual, #116 slide-response, the remaining R6 ownership cleanup plus
registered TS-50/TS-51 timing residuals (the complete-root-Frame/object-workset
cutover is automated-test complete 2026-07-20; three independent re-reviews are
clean, Release builds with zero warnings, and 6,446 tests pass / 5 skip), and
Track MP0.
---

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.