feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 `|Body−LastShadowSyncPos| > 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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@
|
|||
**Source:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR PDB-named decomp).
|
||||
**Cross-check:** `src/AcDream.Core/Physics/InterpolationManager.cs` (current port), `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` (cdb-traced).
|
||||
|
||||
> **2026-07-19 correction:** the May audit correctly identified that
|
||||
> `adjust_offset` mutates its `Frame*`, but incorrectly described the result as
|
||||
> translation-only and the old `Vector3` API as equivalent. Retail preserves
|
||||
> the complete relative quaternion produced by `Position::subtract2` unless
|
||||
> `keep_heading` explicitly replaces that rotation with heading zero. R6 now
|
||||
> carries the complete Frame. See
|
||||
> `docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md`. Section 7 is
|
||||
> retained as a historical snapshot of the May port, not current status.
|
||||
|
||||
---
|
||||
|
||||
## 1. Class layout (verbatim from `acclient.h`)
|
||||
|
|
@ -135,7 +144,9 @@ Our current port does duplicate-prune against only the last entry (`_queue.Last`
|
|||
## 4. `InterpolationManager::adjust_offset` @ `0x00555d30`
|
||||
|
||||
Signature: `void adjust_offset(InterpolationManager *this, Frame *arg2, double arg3)`
|
||||
- `arg2` = output Frame (already-zeroed identity Frame from caller `CPhysicsObj::UpdatePartsInternal` @ `0x00512c3c`).
|
||||
- `arg2` = the mutable per-object delta Frame. `CPartArray::Update` may already
|
||||
have written root translation and orientation into it; active interpolation
|
||||
replaces that complete Frame.
|
||||
- `arg3` = frame `dt` in seconds (double).
|
||||
|
||||
### Critical answer: **arg2 is MUTATED IN PLACE.** Not a delta-return.
|
||||
|
|
@ -146,13 +157,18 @@ The last meaningful action (line 353253) is:
|
|||
00555f10 Frame::operator=(arg2, &__return);
|
||||
```
|
||||
|
||||
where `__return` is a Frame whose `m_fOrigin` was just scaled to the per-frame step, and whose rotation is 0 (or kept-heading) (line 353251). The caller composes this into the world position with:
|
||||
where `__return` is the complete relative Frame from `Position::subtract2`.
|
||||
Only `m_fOrigin` is scaled to the per-frame step. Its quaternion remains the
|
||||
target-relative rotation unless `keep_heading` calls `Frame::set_heading(0)`.
|
||||
The caller composes this into the world position with:
|
||||
|
||||
```c
|
||||
00512d22 Frame::combine(arg3 /*world out*/, &this->m_position.frame, &var_40 /*= arg2*/);
|
||||
```
|
||||
|
||||
**So `adjust_offset` writes a translation-only Frame — the caller treats it as an offset Frame to combine with the body's current position.** Our acdream port returns a `Vector3` delta, which the caller adds to the body — equivalent semantics.
|
||||
**So `adjust_offset` writes a complete relative Frame, and the caller combines
|
||||
both its translation and orientation with the body's current Frame.** A
|
||||
`Vector3`-only port loses pitch/roll and is not equivalent.
|
||||
|
||||
### Step-by-step retail flow:
|
||||
|
||||
|
|
@ -238,7 +254,7 @@ if (this->keep_heading != 0) {
|
|||
}
|
||||
|
||||
// ---- Output ----
|
||||
Frame::operator=(arg2, &delta_frame); // OUT: arg2 = translation-only Frame
|
||||
Frame::operator=(arg2, &delta_frame); // OUT: complete relative Frame
|
||||
```
|
||||
|
||||
### NOTE on `progress_quantum`
|
||||
|
|
@ -456,7 +472,7 @@ if (CPhysicsObj::SetPositionSimple(po, &target, 1) == OK_SPE) {
|
|||
| **Stall fail action** | Increments fail counter; only blips when threshold exceeded INSIDE adjust_offset | Calls `NodeCompleted(0)` on stall and pops the head; UseTime does the actual blip | **Architectural gap.** Retail's NodeCompleted(0) on stall pops the head node (advancing the queue) — useful when the head is unreachable but later nodes might be. Our port leaves the head in place. This means a single bad waypoint can cause repeated 5-frame failures rather than skipping past it. |
|
||||
| Blip target on threshold | `_queue.Last.TargetPosition` (tail) | UseTime: tail OR blipto_position OR last-pos-before-velocity-tail | Mostly OK for our use case (only type-1 nodes). |
|
||||
| `transient_state & 1` gate | None | Required | Probably safe in our codebase; file as follow-up. |
|
||||
| AdjustOffset return shape | `Vector3` delta | In-place mutate `Frame*` (translation-only Frame) | Functionally equivalent — caller composes with current position either way. |
|
||||
| AdjustOffset return shape | `Vector3` delta | In-place mutate complete `Frame*` | **Historical R6 gap, fixed 2026-07-19.** The old shape lost target-relative orientation; production now uses `MotionDeltaFrame`. |
|
||||
|
||||
### Concrete actionable changes
|
||||
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ stage; **LOW** = dormant/textual.
|
|||
| G3 | **update_internal boundary model: clamp-to-`high_frame`/`low_frame` + leftover-time carry, boundary test `floor(frameNum) > high_frame` (i.e. ≥ high+1).** acdream tests `newPos >= maxBoundary - FrameEpsilon` and clamps `_framePosition = maxBoundary - FrameEpsilon` — epsilon-shifted boundary, different clamp target, no strict retail equivalence at exact-integer landings. This is the #61 "link→cycle boundary flash" bug class. | `0x005255d0` body (lines 301839-302235); ACE `Sequence.cs:366-377` (fwd), `:394-406` (rev) | `AnimationSequencer.cs:894,900` (`maxBoundary - FrameEpsilon`) | **BLOCKER** |
|
||||
| G4 | **`safety=64` loop cap** — retail has none; termination comes from `frameTimeElapsed` shrinking + the `frametime == 0` branch returning. Mandate says delete bandaids on cutover. | ACE `Sequence.cs:351-443` (no cap); decomp `0x005255d0` (while-true loop) | `AnimationSequencer.cs:872-874` | MED (delete in R1 core) |
|
||||
| G5 | **AnimDone gate is a LIST-STRUCTURE test, not a node flag.** Retail queues the global `anim_done_hook` singleton when `animDone && hook_obj != null && (anim_list.head_ − 4) != first_cyclic` — i.e. "the old head has been consumed and we're not already in the cyclic tail". acdream pushes `AnimationDoneSentinel` gated on `!_currNode.Value.IsLooping` — a per-node flag that acdream itself invented (retail nodes have no IsLooping). Different gate ⇒ different MotionDone timing. R2's `CheckForCompletedMotions → AnimationDone → MotionDone` chain consumes this signal; it must be retail-exact in R1. | `0x00525943-0x00525968` (verified raw this pass); AnimDoneHook singleton at data `0x0081d9fc`, Execute `0x00526c20` → `Hook_AnimDone 0x0050fda0` → `CPartArray::AnimationDone(1)` (§18a) | `AnimationSequencer.cs:1129` (`AnimationDoneSentinel`), Advance's `!IsLooping` gate (r1-acdream map row "AnimationDone hook") | **HIGH** |
|
||||
| G6 | **Two-stage hook dispatch.** Retail `execute_hooks` QUEUES matching `CAnimHook*` into `CPhysicsObj.anim_hooks` (SmartArray); `CPhysicsObj::process_hooks` executes them inside `UpdatePositionInternal`, before the transition and manager tail, then resets `m_num=0`. acdream's `_pendingHooks` + `ConsumePendingHooks()` is structurally similar. R6 processes semantic `AnimationDone` at capture time while retaining pose-dependent presentation delivery until current root/part poses are published. | `execute_hooks 0x00524830` (line 300780), `add_anim_hook 0x00514c20` (line 282906), `process_hooks 0x00511550` (line 279431) (§18); corrected order pinned in `2026-07-19-r6-update-object-order-pseudocode.md` | `AnimationSequencer.cs` pending-hook queue; `AnimationHookFrameQueue.Capture` semantic completion and `Drain` presentation delivery | RETIRED by R6 semantic-order cutover 2026-07-19 |
|
||||
| G7 | **`apply_physics` + Frame-target root motion is unwired.** Retail: `update(quantum, Frame*)` accumulates `velocity*signed_quantum` into `frame.m_fOrigin` and `omega*signed_quantum` via `Frame::rotate`, with `signed_quantum = copysign(fabs(arg3), arg4)`; per crossed frame the quantum is `1.0/framerate` signed by elapsed. acdream has NO `apply_physics`: velocity/omega are surfaced as `CurrentVelocity`/`CurrentOmega` properties consumed by external movement code, and pos-frame root motion accumulates into `_rootMotionPos/_rootMotionRot` that **nothing drains** (`ConsumeRootMotionDelta()` has zero callers). | `0x00524ab0` (line 300955, §19); `Frame::rotate 0x004525b0` | `AnimationSequencer.cs:975-979` region (`ConsumeRootMotionDelta`, dead); `CurrentVelocity/CurrentOmega` consumers `GameWindow.cs:9331-9334`, `:12917` | **HIGH** — this is retail's entire physics-from-animation mechanism; R6's `CPartArray.Update → adjust_offset → Frame.combine` tick order needs it |
|
||||
| G8 | **Empty-list physics-only fallback.** Retail `update`: if `anim_list` empty and `frame != null` → `apply_physics(frame, elapsed, elapsed)` (accumulated velocity still moves the object — free-fall/knockback with no anim). acdream `Advance` with no `_currNode` does nothing. | `0x00525b80` (line 302402, §22) | `AnimationSequencer.cs:874` (`while (... && _currNode != null ...)` — falls out) | MED |
|
||||
| G6 | **Two-stage hook dispatch.** Retail `execute_hooks` QUEUES matching `CAnimHook*` into `CPhysicsObj.anim_hooks` (SmartArray); `CPhysicsObj::process_hooks` executes the complete stream inside `UpdatePositionInternal`, before the transition and manager tail, then resets `m_num=0`. acdream's `_pendingHooks` + `ConsumePendingHooks()` is structurally similar. R6 processes semantic `AnimationDone` at capture time while retaining every other pose-dependent hook until current root/part/equipped-child poses are published. | `execute_hooks 0x00524830` (line 300780), `add_anim_hook 0x00514c20` (line 282906), `process_hooks 0x00511550` (line 279431) (§18); corrected order pinned in `2026-07-19-r6-complete-root-frame-pseudocode.md` | `AnimationSequencer.cs` pending-hook queue; `AnimationHookFrameQueue.Capture` semantic completion and `Drain` presentation delivery | **PARTIAL:** AnimationDone semantic order retired 2026-07-19; non-AnimationDone delivery remains TS-50 |
|
||||
| G7 | **Frame-target root motion.** `update(quantum, Frame*)` accumulates literal velocity and omega plus authored PosFrames into one Frame. Local and remote movement now pass that complete Frame through `PositionManager`, `Frame::combine`, and collision; command-derived translation and manual rotation consumers are gone. | `0x00524ab0` (line 300955, §19); `Frame::rotate 0x004525b0`; `Frame::combine 0x005122E0` | `CSequence.ApplyPhysics`; `AnimationSequencer.Advance(dt, Frame)`; `MotionDeltaFrame`; `PlayerMovementController`; `RemotePhysicsUpdater`; `2026-07-19-r6-complete-root-frame-pseudocode.md` | **RETIRED 2026-07-19** |
|
||||
| G8 | **Empty-list physics-only fallback.** Retail `update`: if `anim_list` is empty and `frame != null` → `apply_physics(frame, elapsed, elapsed)` (accumulated velocity still moves the object — free-fall/knockback with no animation). The ported `CSequence.Update` implements that branch and `AnimationSequencer.Advance(dt, Frame)` exposes it to the ordinary-object scheduler. | `0x00525b80` (line 302402, §22) | `CSequence.Update`; `AnimationSequencer.Advance(dt, Frame)`; empty-list/full-Frame scheduler conformance tests | **RETIRED 2026-07-19** |
|
||||
| G9 | **`advance_to_next_animation` uses elapsed-direction plus node-framerate sign gates.** For elapsed ≥ 0, subtract the outgoing pose only when its framerate is negative, step Next/wrap `first_cyclic`, and combine the incoming pose only when its framerate is positive. For elapsed < 0, subtract when outgoing framerate is non-negative, step Previous/wrap list tail, and combine when incoming framerate is negative. Physics inside a selected pose operation uses the separate strict `abs(framerate) > F_EPSILON` gate. Matching v11.4186 assembly and ACE agree; the earlier “four unconditional pose ops” cleanup was wrong. | `0x005252B0`; matching assembly audit 2026-07-19; `r1-ace-sequence.md:70-86` | `CSequence.AdvanceToNextAnimation` + exact-boundary/sign conformance tests | **RETIRED 2026-07-19** |
|
||||
| G10 | **`append_animation` slides `first_cyclic` to the just-appended node on EVERY call.** Retail's "cyclic tail" is always exactly the LAST appended anim (so a multi-anim cycle MotionData loops only its final AnimData node once earlier ones are consumed). acdream sets `_firstCyclic` to the FIRST node of the cycle MotionData. Also retail: `if (curr_anim == null) { curr_anim = head; frame_number = get_starting_frame(head); }` — acdream's equivalents are scattered through `SetCycle`'s rebuild. | `0x00525510` (line 301777, §24) | `AnimationSequencer.cs:634-645` region + `EnqueueMotionData` (r1-acdream map rows "Node list", "add_motion") | **HIGH** — divergent loop membership for multi-anim cycles; also the retail invariant that makes remove_cyclic/apricot correct |
|
||||
| G11 | **The remove-family with curr_anim snap semantics is missing.** Retail: `remove_cyclic_anims` (0x00524e40) deletes `first_cyclic`→tail, snapping `curr_anim` back to prev + `frame_number = get_ending_frame(prev)` (or 0), then `first_cyclic = tail`; `remove_link_animations(n)` (0x00524be0) / `remove_all_link_animations` (0x00524ca0) delete predecessors of `first_cyclic`, snapping `curr_anim` FORWARD to `first_cyclic` + `get_starting_frame`; `clear_animations` (0x00524dc0) full wipe; `apricot` (0x00524b40) trims consumed leading nodes after every update, bounded by `curr_anim`/`first_cyclic`. acdream instead has `ClearCyclicTail` + wholesale queue clears + the invented "stale-head `_currNode` force-relocation" + "Fix B link-skip" — all approximations of what the retail remove-family + apricot do naturally. | lines 301258/301060/301128/301207/300978 (§5-8, §20) | `AnimationSequencer.cs:1311` (`ClearCyclicTail`), `:511`, stale-head relocation + Fix B blocks in `SetCycle` (r1-acdream map rows "Stale-head handling", "Fix B") | **HIGH** — these retire two invented mechanisms |
|
||||
|
|
@ -79,7 +79,7 @@ divergence-register row when touched.
|
|||
| Per-frame crossing walk fires pose+hooks for EVERY integer frame crossed, strict ascending (fwd) / descending (rev) order | `0x005255d0` do/while loops (lines 302006-302056 + reverse mirror) | `AnimationSequencer.cs:910-941` (fwd `lastFrame++` w/ Forward, rev `lastFrame--` w/ Backward) |
|
||||
| Forward node wrap to `first_cyclic` (loop-the-cycle mechanism) | `0x005252b0` @0x005253xx: `GetNext==null → first_cyclic` | `AnimationSequencer.cs:1350-1358` |
|
||||
| Leftover-time carry into the next node after a boundary (multi-node fast-forward in one tick) | Matching v11.4186 assembly `0x00525982-0x00525990` copies `frameTimeElapsed` back then loops; ACE `Sequence.cs:436-442` agrees | `CSequence.UpdateInternal` assigns `timeElapsed = frameTimeElapsed` before looping |
|
||||
| Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse | `Frame::combine`/`Frame::subtract1` call sites in `0x005255d0`/`0x005252b0` | `ApplyPosFrame(node, idx, reverse:)` fwd/conjugate-reverse (r1-acdream map row "Root motion") — values correct, TARGET wrong (G7: accumulator never drained) |
|
||||
| Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse | `Frame::combine`/`Frame::subtract1` call sites in `0x005255d0`/`0x005252b0` | `CSequence` writes the caller-supplied root Frame; local and remote physics consume that same Frame (G7 retired) |
|
||||
| `frame_number` floored to int for pose lookup (`get_curr_animframe`/`get_curr_frame_number` shape) | `0x00524970`/`0x005249d0` (§15/17) | `AnimationSequencer.cs:884` (`(int)Math.Floor(_framePosition)`) |
|
||||
| `clear_physics` zeroing before rebuild | `0x00524d50` + `GetObjectSequence`'s `clear_physics; remove_cyclic_anims` pairing @0x005229cf etc. | `ClearPhysics()` called from `SetCycle` (r1-acdream map row "clear_physics") |
|
||||
| `AnimData` speed scaling: only framerate × speed, low/high pass through | `operator* 0x00525d00` (invoked from `add_motion` @0x0052255b) | `LoadAnimNode` (`AnimData.Framerate * speedMod`) |
|
||||
|
|
|
|||
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal file
408
docs/research/2026-07-19-r6-complete-root-frame-pseudocode.md
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
# R6 complete root Frame cutover — retail pseudocode
|
||||
|
||||
Date: 2026-07-19
|
||||
|
||||
Scope: the translation and orientation emitted by `CSequence` and consumed by
|
||||
`CPhysicsObj::UpdatePositionInternal`. This note replaces the former acdream
|
||||
split between animation root translation and command-derived/manual rotation.
|
||||
|
||||
## Named retail oracle
|
||||
|
||||
- `Frame::combine` `0x005122E0` (`acclient_2013_pseudo_c.txt:280355`)
|
||||
- `Frame::set_rotate` `0x00535080`
|
||||
- `Frame::IsValid` `0x00534ED0`
|
||||
- `Position::subtract2` (complete relative Position/Frame)
|
||||
- `CPhysicsObj::UpdatePositionInternal` `0x00512C30` (`:280817`)
|
||||
- `CPhysicsObj::UpdateObjectInternal` `0x005156B0`
|
||||
- `CPhysicsObj::SetPositionInternal(CTransition const*)` `0x00515330`
|
||||
- `CPhysicsObj::change_cell` `0x00513390`
|
||||
- `CPhysicsObj::remove_shadows_from_cells` `0x00511230`
|
||||
- `CPhysicsObj::add_shadows_to_cells` `0x00514AE0`
|
||||
- `CPhysicsObj::update_object` `0x00515D10`
|
||||
- `CPhysicsObj::prepare_to_enter_world` `0x00511FA0`
|
||||
- `CPhysicsObj::set_active` `0x0050FC40`
|
||||
- `CPhysicsObj::animate_static_object` `0x00513DF0`
|
||||
- `CPhysics::UseTime` `0x00509950` (separate static-object loop)
|
||||
- `CPartArray::Update` `0x00517DB0` (`:285883`)
|
||||
- `add_motion` `0x005224B0` (`:298437`)
|
||||
- `CSequence::apply_physics` `0x00524AB0` (`:300955`)
|
||||
- `InterpolationManager::InterpolateTo` `0x00555B20`
|
||||
- `InterpolationManager::adjust_offset` `0x00555D30`
|
||||
|
||||
The installed retail portal DAT is a second executable fixture. Motion table
|
||||
`0x09000001` (Humanoid), global `TurnRight` modifier `0x0000000D`, has
|
||||
`HasOmega` and literal omega `(0, 0, -1.5)` radians/second. It is not
|
||||
`-pi/2`, and it is not absent. The fixture is pinned by
|
||||
`HumanoidMotionTableRootMotionTests`.
|
||||
|
||||
ACE cross-reference:
|
||||
`docs/research/2026-07-02-r2-motiontable/r2-ace-motiontable.md` records the
|
||||
ACE `MotionTable`/`Sequence` implementation: modifier lookup tries the styled
|
||||
key then the unstyled key, and `add_motion` writes
|
||||
`MotionData.Velocity * speed` plus `MotionData.Omega * speed`. ACE agrees with
|
||||
the named client here. Retail remains the ordering and transform oracle.
|
||||
|
||||
## Pseudocode
|
||||
|
||||
```text
|
||||
add_motion(sequence, motionData, speed):
|
||||
if motionData is null:
|
||||
return
|
||||
|
||||
sequence.velocity = motionData.velocity * speed
|
||||
sequence.omega = motionData.omega * speed
|
||||
for animation in motionData.animations:
|
||||
sequence.append_animation(animation * speed)
|
||||
|
||||
apply_physics(sequence, frame, magnitudeQuantum, signSource):
|
||||
signedQuantum = abs(magnitudeQuantum)
|
||||
if signSource < 0:
|
||||
signedQuantum = -signedQuantum
|
||||
|
||||
frame.origin += sequence.velocity * signedQuantum
|
||||
frame.rotate(sequence.omega * signedQuantum)
|
||||
|
||||
CPartArray.Update(dt, localFrame):
|
||||
sequence.update(dt, localFrame)
|
||||
|
||||
CPhysicsObj.prepare_to_enter_world(now):
|
||||
update_time = now
|
||||
remove object and children from lost/destroy queues
|
||||
if not Static:
|
||||
transient_state.Active = true
|
||||
|
||||
CPhysicsObj.update_object(now):
|
||||
if parent exists or cell is null or Frozen:
|
||||
transient_state.Active = false
|
||||
return without consuming update_time
|
||||
|
||||
if player_object exists:
|
||||
player_vector = offset(player.position, object.position)
|
||||
player_distance = length(player_vector)
|
||||
if player_distance <= 96 world units or partArray is null:
|
||||
set_active(true)
|
||||
else:
|
||||
transient_state.Active = false
|
||||
// With no player_object, retain the prior Active state.
|
||||
|
||||
elapsed = now - update_time
|
||||
if elapsed <= F_EPSILON or elapsed > HugeQuantum:
|
||||
update_time = now
|
||||
return
|
||||
|
||||
while elapsed > MaxQuantum:
|
||||
UpdateObjectInternal(MaxQuantum)
|
||||
update_time += MaxQuantum
|
||||
elapsed -= MaxQuantum
|
||||
|
||||
if elapsed > MinQuantum:
|
||||
UpdateObjectInternal(elapsed)
|
||||
update_time = now
|
||||
// A remainder at or below MinQuantum stays pending.
|
||||
|
||||
CPartArray.InitDefaults(setup):
|
||||
if setup.DefaultAnimation != 0:
|
||||
sequence.clear_animations()
|
||||
sequence.append_animation(
|
||||
animation = setup.DefaultAnimation,
|
||||
low = 0,
|
||||
high = -1,
|
||||
framerate = 30)
|
||||
owner.InitDefaults(setup)
|
||||
|
||||
CPhysicsObj.InitDefaults(setup):
|
||||
if setup.DefaultScript != 0:
|
||||
play_script_internal(setup.DefaultScript)
|
||||
install setup default motion/sound/physics-script tables
|
||||
if state has Static:
|
||||
if setup.DefaultAnimation != 0:
|
||||
state |= HasDefaultAnim
|
||||
if setup.DefaultScript != 0:
|
||||
state |= HasDefaultScript
|
||||
if state has HasDefaultAnim or HasDefaultScript:
|
||||
CPhysics.AddStaticAnimatingObject(this)
|
||||
|
||||
CPhysicsObj.animate_static_object(now):
|
||||
if cell is null:
|
||||
return
|
||||
elapsed = now - update_time
|
||||
if elapsed <= F_EPSILON or elapsed > 2 seconds:
|
||||
update_time = now
|
||||
return
|
||||
if partArray exists:
|
||||
if state has HasDefaultAnim:
|
||||
partArray.Update(elapsed, null)
|
||||
// Retail passes the stored vector directly; no elapsed multiply.
|
||||
position.frame.grotate(angularVelocity)
|
||||
UpdatePartsInternal()
|
||||
UpdateChildrenInternal()
|
||||
if state has HasDefaultScript and scriptManager exists:
|
||||
scriptManager.UpdateScripts()
|
||||
if particleManager exists:
|
||||
particleManager.UpdateParticles()
|
||||
process_hooks()
|
||||
update_time = now
|
||||
|
||||
CPhysics.UseTime(now):
|
||||
for object in ordinary object table:
|
||||
object.update_object(now)
|
||||
for object in static_animating_objects:
|
||||
object.animate_static_object(now)
|
||||
// These are separate ownership/workset paths. InitDefaults registers any
|
||||
// physics-Static object carrying Setup DefaultAnimation/DefaultScript,
|
||||
// including a server-created live object.
|
||||
|
||||
CPhysicsObj.UpdatePositionInternal(dt, outputWorldFrame):
|
||||
localFrame = identity
|
||||
|
||||
if not Hidden:
|
||||
if partArray exists:
|
||||
partArray.Update(dt, localFrame)
|
||||
|
||||
if OnWalkable:
|
||||
localFrame.origin *= objectScale
|
||||
else:
|
||||
localFrame.origin *= 0
|
||||
// Orientation is never cleared or scaled by this gate.
|
||||
|
||||
if positionManager exists:
|
||||
positionManager.adjust_offset(localFrame, dt)
|
||||
|
||||
outputWorldFrame = combine(position.frame, localFrame)
|
||||
|
||||
if not Hidden:
|
||||
UpdatePhysicsInternal(dt, outputWorldFrame)
|
||||
|
||||
process_hooks()
|
||||
|
||||
Frame.combine(output, current, delta):
|
||||
output.origin = current.origin + rotate(current.orientation, delta.origin)
|
||||
output.set_rotate(current.orientation * delta.orientation)
|
||||
|
||||
Frame.set_rotate(candidate):
|
||||
previous = orientation
|
||||
orientation = normalize(candidate)
|
||||
if not Frame.IsValid():
|
||||
orientation = previous
|
||||
|
||||
InterpolationManager.adjust_offset(delta, dt):
|
||||
if no Position node or body is not in Contact:
|
||||
return without mutating delta
|
||||
relative = Position.subtract2(target, current)
|
||||
relative.origin = clamp_to_catchup_distance(relative.origin, dt)
|
||||
if manager.keep_heading:
|
||||
relative.frame.set_heading(0)
|
||||
delta = relative.frame // replaces both origin and orientation
|
||||
|
||||
CPhysicsObj.SetPositionInternal(transition):
|
||||
if transition.current_cell is null:
|
||||
prepare_to_leave_visibility()
|
||||
store_position(transition.current_position)
|
||||
object_maintenance.goto_lost_cell()
|
||||
clear Active
|
||||
return
|
||||
|
||||
if current cell differs:
|
||||
change_cell(transition.current_cell)
|
||||
else:
|
||||
commit the new full objcell id to the object, PartArray, and children
|
||||
set_frame(transition.current_position.frame)
|
||||
commit contact plane, OnWalkable, sliding normal, and collision callbacks
|
||||
if transition has a non-empty crossed-cell array:
|
||||
remove_shadows_from_cells()
|
||||
add_shadows_to_cells(transition.cell_array)
|
||||
```
|
||||
|
||||
The ordering consequence is important: a delta translates through the old
|
||||
orientation and only then applies its relative rotation. A large elapsed frame
|
||||
must run multiple complete object quanta in time order. A render fragment below
|
||||
the strict minimum does not advance CSequence at all; it stays in the object
|
||||
clock until a later admitted quantum.
|
||||
|
||||
## Port mapping
|
||||
|
||||
- `MotionDeltaFrame.Combine` implements the retail composition and scales only
|
||||
the incoming origin.
|
||||
- One incarnation-stable `RetailObjectQuantumClock` is shared by local,
|
||||
remote, Hidden, and projectile components of each live object. It admits
|
||||
complete object quanta; the local animation callback writes one reusable
|
||||
`MotionDeltaFrame` only after admission. `prepare_to_enter_world` clears the
|
||||
retained fragment and immediately restores Active only for non-Static
|
||||
objects, so their next elapsed frame can run; Static objects retain the
|
||||
inactive state established by initialization/leave-visibility. It does not
|
||||
invent a second reactivation frame.
|
||||
- `CPartArray::InitDefaults` installs Setup DefaultAnimation for every
|
||||
setup-backed PartArray, independent of Static. Static controls only the
|
||||
additional `static_animating_objects -> animate_static_object` workset.
|
||||
Both DAT-hydrated statics and server-created objects whose final physics
|
||||
state is Static enter that workset when their Setup carries a
|
||||
DefaultAnimation. Default animation bypasses the MotionTable and installs
|
||||
the Setup animation directly over `0..last` at 30 frames/second. A short
|
||||
cell-less interval remains on the owner's clock and catches up after
|
||||
re-entry; an interval above two seconds is rebased and discarded. Script-only
|
||||
static owners still use acdream's shared PhysicsScriptRunner and are covered
|
||||
by TS-51 rather than entering an empty animation scan.
|
||||
A live static workset entry binds the exact PartArray and PhysicsBody already
|
||||
owned by its `LiveEntityRecord`; it never constructs a second sequencer or
|
||||
body. The raw angular-velocity vector is applied without multiplying by
|
||||
elapsed time, exactly as the named function does. A changed root commits the
|
||||
entity, effect pose, and multipart collision shadow together. Zero omega does
|
||||
not trigger a collision reflood, and a Hidden/nonresident object cannot have
|
||||
its suspended shadow restored by this animation path.
|
||||
- Animation hooks are captured after `UpdatePhysicsInternal` and before the
|
||||
transition/manager tail. Semantic `AnimationDone` executes there, matching
|
||||
`CPhysicsObj::process_hooks`. Other pose-dependent router sinks remain
|
||||
deferred until final root/part/equipped-child pose publication; that known
|
||||
residual is registered as TS-50 rather than claimed as exact ordering.
|
||||
Static owners retain their semantic hook tail until final root, part, and
|
||||
attached-child publication, matching `animate_static_object`'s later
|
||||
`process_hooks` call. A residency change invalidates both the prepared pose
|
||||
and that pending tail.
|
||||
- Retail session dispatch and `CPhysics::UseTime` are single-threaded FIFO
|
||||
operations. App therefore wraps complete packet and object-frame operations
|
||||
in `RetailInboundEventDispatcher`: a callback-triggered packet queues behind
|
||||
the current operation instead of recursively interleaving with its tail.
|
||||
Position, State, Vector, and Movement keep separate authority versions;
|
||||
Position/Vector/Movement additionally share only a velocity-write version.
|
||||
Never replace these with one global body-mutation token: that incorrectly
|
||||
discards independent accepted channels (for example State during Position).
|
||||
The direct packet/frame path carries state into a cached static callback and
|
||||
allocates nothing; only genuinely nested work receives a retained FIFO node.
|
||||
- Projectile `BeginQuantum` is candidate-only. It restores the canonical begin
|
||||
frame/dynamics before returning; `CompleteQuantum` may commit only while the
|
||||
exact live record, projectile runtime, entity, spatial owner, and prediction
|
||||
version remain current. Any accepted Position, Vector, or State invalidates
|
||||
the candidate without rolling back the authoritative mutation.
|
||||
- Animation-hook batches capture the pose owner's monotonic lifetime before
|
||||
running semantic AnimationDone. Capture rechecks it before every completion,
|
||||
and drain rechecks that lifetime and root pose before every routed hook,
|
||||
because either AnimationDone or an earlier sink can delete the owner and
|
||||
reuse the same local ID synchronously.
|
||||
- Remote animation advances into one reusable DAT `Frame`; both components are
|
||||
passed to `RemotePhysicsUpdater` and applied through the same
|
||||
`PositionManager` frame.
|
||||
- A body-backed animated object without a MovementManager now commits its
|
||||
complete Frame through `LiveEntityOrdinaryPhysicsUpdater`: root translation
|
||||
is admitted only while OnWalkable, orientation remains live, the resulting
|
||||
candidate runs through Transition/SetPositionInternal, and the same record is
|
||||
rebucketed without recreating resources. Callback deletion/replacement is
|
||||
revalidated before every commit.
|
||||
- The resolved body and `WorldEntity` root/full-cell frame are committed before
|
||||
the canonical rebucket callback, matching SetPositionInternal's frame/contact
|
||||
commit before its final shadow replacement. A loaded-to-pending rebucket (or
|
||||
same-GUID replacement during its visibility callback) invalidates the caller
|
||||
before shadow or manager-tail publication. Non-projectile shadow residency is
|
||||
symmetric in `LiveEntityPresentationController`, including the pending-first
|
||||
case whose initial false edge precedes collision registration; projectiles
|
||||
retain their dedicated residency owner.
|
||||
- Local projection and both authoritative remote UpdatePosition tails use the
|
||||
same post-rebucket exact-owner gate. Remote collision refresh is forced by a
|
||||
cell change or by a changed complete pose (translation or sign-invariant
|
||||
quaternion), so offset/multipart Setup shapes rotate in place and a same-pose
|
||||
cell crossing cannot retain an old flood seed.
|
||||
- A retained projectile remains the same CPhysicsObj when Missile clears. With
|
||||
no MovementManager it continues through the ordinary one-sphere transition
|
||||
owner; with a MovementManager the owner transfers exactly once to remote
|
||||
motion. Projection re-entry restores its retained collision shadow on the
|
||||
visibility edge, before any later simulation quantum.
|
||||
- Active interpolation preserves the target's complete quaternion and replaces
|
||||
the PartArray Frame. The already-close `InterpolateTo` branch applies only
|
||||
the target heading, matching `CPhysicsObj::set_heading`. `keep_heading` is
|
||||
manager-wide, not a per-node flag; each near enqueue replaces it.
|
||||
- `FrameOps.SetRotate` rejects zero, non-finite, and otherwise invalid
|
||||
quaternion candidates by restoring the prior orientation.
|
||||
- Spawn, teleport, correction, and outbound movement/position serialization
|
||||
read the authoritative body quaternion directly; none round-trips through
|
||||
the horizontal `Yaw` projection.
|
||||
- `MotionTableDispatchSink` only dispatches motion-table commands. It no longer
|
||||
reports turn callbacks.
|
||||
- `RemoteMotion.ObservedOmega`, the player/NPC multiplication-order fork, and
|
||||
`AnimationSequencer.SetCycle`'s synthetic `pi/2` turn omega are deleted.
|
||||
- A controller deliberately constructed without a `PartArray` retains a
|
||||
Humanoid-only fallback at the DAT-pinned `1.5` radians/second. A normal GUI
|
||||
client does not enter that path.
|
||||
|
||||
## Conformance gates
|
||||
|
||||
- `MotionDeltaFrameTests`: exact `FrameOps.Combine` equivalence, scale affects
|
||||
origin only, and temporal turn-then-move composition.
|
||||
- `RetailObjectQuantumClockTests`: strict bounds, retained remainder,
|
||||
MaxQuantum subdivision, HugeQuantum discard, `set_active` rebasing, and
|
||||
Static-aware `prepare_to_enter_world` reset.
|
||||
- `RetailObjectActivityGateTests` and `LiveEntityRuntimeTests`: the 96-unit
|
||||
PartArray gate, parent/cell/Frozen suspension, loaded rebucket retention,
|
||||
pending re-entry rebasing, body/clock Active synchronization, and GUID-reuse
|
||||
clock isolation.
|
||||
- `LiveEntityAnimationSchedulerTests`: the canonical live-object workset also
|
||||
advances objects without a render animation, consumes their queued near
|
||||
UpdatePosition correction, applies complete generic root
|
||||
Frames, transfers a retained missile body exactly once when classification
|
||||
clears, and stops catch-up immediately when a callback deletes, withdraws,
|
||||
or replaces an incarnation.
|
||||
- `RetailStaticAnimatingObjectSchedulerTests` and
|
||||
`AnimationSequencerTests`: unconditional Setup DefaultAnimation installation,
|
||||
DAT/live physics-Static workset membership, direct 30-fps playback,
|
||||
short-residency-gap catch-up, canonical PartArray/body reuse, raw (not
|
||||
elapsed-scaled) omega, synchronized multipart shadow rotation, zero-omega
|
||||
no-op commits, callback-safe removal, and the retail greater-than-two-second
|
||||
discard. `StaticLiveRootCommitter` coverage proves Hidden omega cannot
|
||||
resurrect a suspended shadow.
|
||||
- `RemoteInboundMotionDispatcherTests`: one animation-optional owner preserves
|
||||
retail packet-head/style/type-0/sticky ordering with a null PartArray sink;
|
||||
unknown movement types execute the head and never enter the case-0 funnel;
|
||||
supersession during interrupt prevents every later packet side effect.
|
||||
- `LiveEntityOrdinaryPhysicsUpdaterTests`: complete root Frames cross cells and
|
||||
landblocks through Transition, while hook-side deletion prevents stale body,
|
||||
shadow, pose, or spatial commits.
|
||||
- `ProjectileControllerTests`: pending-cell hydration restores the same body's
|
||||
Active/InWorld/shadow residency immediately and does not replay logical
|
||||
creation; the destination cell is canonical before suspension and after
|
||||
hydration, and authoritative Position/Vector/State between prediction halves
|
||||
discards only the stale candidate.
|
||||
- `RetailInboundEventDispatcherTests`, `LiveEntityPresentationControllerTests`,
|
||||
`RemoteTeleportControllerTests`, and `EntityEffectPoseRegistryTests`: nested
|
||||
packet arrival drains after the complete older tail, independent State and
|
||||
Position both survive resolver callbacks, newer Position supersedes older
|
||||
placement, Hidden-to-Visible recursion remains FIFO, and hook batches cannot
|
||||
cross deletion/local-ID reuse during capture or drain. The FIFO's nonnested
|
||||
state path is pinned at zero allocations after warmup.
|
||||
- `PlayerMovementControllerTests`: full-frame local composition, large-frame
|
||||
equivalence, hidden/airborne gates, hook cadence, and proof that
|
||||
animation-backed turns are not integrated twice.
|
||||
- `InterpolationManagerTests`: complete relative target orientation,
|
||||
keep-heading, and already-close heading-only behavior.
|
||||
- `RemotePhysicsUpdaterTests` and `LiveEntityPresentationControllerTests`:
|
||||
translation uses the pre-turn orientation and the same frame then rotates the
|
||||
body; cross-cell loaded-to-pending commits the destination root before the
|
||||
callback boundary, suspends rather than re-adds collision, restores on
|
||||
hydration, and isolates old shadows across callback GUID reuse. Direct
|
||||
authoritative rebuckets and initial-pending materialization cover the same
|
||||
symmetric collision-residency contract; same-position rotation and cell-only
|
||||
changes refresh collision while unchanged/sign-flipped quaternion poses do
|
||||
not reflood.
|
||||
- `LiveEntityRuntimeTests`: update iteration and render-ID classification copy
|
||||
only concrete spatial animation owners through reusable storage with zero
|
||||
steady-state allocations.
|
||||
- `MotionTableDispatchSinkTests`: Branch-4 turn preserves the locomotion cycle,
|
||||
contributes literal DAT omega, and unwinds on stop.
|
||||
- `RemoteChaseEndToEndHarnessTests`: turn, chase, re-arm, action completion, and
|
||||
sticky following run through the complete Frame plus retail manager tail.
|
||||
- `HumanoidMotionTableRootMotionTests`: installed retail walk displacement and
|
||||
exact Humanoid `TurnRight` omega `-1.5` radians/second.
|
||||
- `MovementManagerTests` and `PlayerMovementControllerTests`:
|
||||
`PerformMovement` invokes `set_active(1)` before request validation, while a
|
||||
Static physics object preserves retail's no-op activation behavior.
|
||||
|
||||
## Registered residual
|
||||
|
||||
The ordinary/static object worksets and their root/animation clocks are now
|
||||
retail-shaped, but acdream's shared particle and PhysicsScript managers still
|
||||
advance once per render frame after every owner has run. Retail advances those
|
||||
tails inside each admitted object update (and inside
|
||||
`animate_static_object`). This timing difference is deliberately registered as
|
||||
TS-51; it is not part of the R6 conformance claim.
|
||||
|
||||
Final automated gate (2026-07-20): three independent retail-conformance,
|
||||
architecture/integration, and adversarial re-reviews reported no actionable
|
||||
finding after correction; Release build completed with zero warnings; the full
|
||||
solution passed 6,446 tests with 5 intentional skips.
|
||||
Loading…
Add table
Add a link
Reference in a new issue