Compare commits
No commits in common. "a3f53c2644264cb981e4576b60b7559bf45d4720" and "77b59d89e22d464d7285a2b0cb823caf110f3da6" have entirely different histories.
a3f53c2644
...
77b59d89e2
22 changed files with 301 additions and 5631 deletions
35
CLAUDE.md
35
CLAUDE.md
|
|
@ -548,41 +548,6 @@ via `PlayerMovementController.ApplyServerRunRate`) or from
|
|||
(default 2 = 5×5).
|
||||
- `ACDREAM_NO_AUDIO=1` — suppress OpenAL init for headless / driver-
|
||||
broken setups.
|
||||
- `ACDREAM_REMOTE_VEL_DIAG=1` — dump per-tick / per-UM remote motion
|
||||
diagnostics (`[UM_RAW]`, `[SCFAST]`, `[SCFULL]`, `[SETCYCLE]`,
|
||||
`[FWD_WIRE]`, `[OMEGA_DIAG]`, `[SEQSTATE]`, `[PARTSDIAG]`,
|
||||
`[VEL_DIAG]`, `[UPCYCLE]`). Heavy.
|
||||
- ⚠️ `ACDREAM_INTERP_MANAGER=1` — **DO NOT ENABLE.** This was an
|
||||
experimental rewrite (e94e791) of the per-tick remote motion path.
|
||||
It's regressed: the env-var path drops the per-tick collision sweep
|
||||
(`ResolveWithTransition`) that the default path retains, causing a
|
||||
visible "staircase" pattern when remotes run up/down slopes (body
|
||||
Z stays flat between UPs, snaps at each one) plus position blips
|
||||
during steady-state motion. Default (env-var unset) uses the
|
||||
working retail-port chain. The PositionManager class itself is
|
||||
fine and retail-faithful; only the integration into per-tick was
|
||||
wrong. To be re-done in a future L.3 follow-up phase as additive
|
||||
refinement on top of the working chain.
|
||||
|
||||
### Outbound motion wire format (acdream → ACE)
|
||||
|
||||
Important quirk for cross-checking observed remote behavior. acdream's
|
||||
`PlayerMovementController` + `MoveToState` builder encode motion as:
|
||||
|
||||
| Local input | Wire `ForwardCommand` | Wire `HoldKey` | Wire `ForwardSpeed` |
|
||||
|---|---|---|---|
|
||||
| W (run) | `WalkForward` (0x05) | `Run` (2) | server runRate (~2.4–2.94) |
|
||||
| W + Shift (walk) | `WalkForward` (0x05) | `None` (1) | 1.0 |
|
||||
|
||||
ACE auto-upgrades `WalkForward + HoldKey.Run` → `RunForward (0x07)`
|
||||
when relaying to remote observers. So our INBOUND parser sees
|
||||
`fwd=0x07` for "remote is running." This matches retail's encoding.
|
||||
|
||||
When the local player toggles Shift while keeping W held (Run↔Walk
|
||||
demote/promote), acdream sends a fresh `MoveToState` with the new
|
||||
HoldKey + ForwardSpeed. Retail's outbound likely does the same, but
|
||||
ACE's behavior on relay is uncertain — see `#L.X` in ISSUES.md for
|
||||
the open Run↔Walk cycle bug on observed retail-driven remotes.
|
||||
|
||||
### Visual verification workflow
|
||||
|
||||
|
|
|
|||
148
docs/ISSUES.md
148
docs/ISSUES.md
|
|
@ -46,154 +46,6 @@ Copy this block when adding a new issue:
|
|||
|
||||
# Active issues
|
||||
|
||||
## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** MEDIUM (visible animation desync; not a correctness/wire bug)
|
||||
**Filed:** 2026-05-03
|
||||
**Component:** physics / motion / animation
|
||||
|
||||
**Description:** When observing a remote-driven player character through
|
||||
acdream and the actor toggles Shift while keeping a direction key held
|
||||
(Run↔Walk demote/promote), the visible leg cycle does NOT update on the
|
||||
observer side. Body position eventually corrects via UpdatePosition
|
||||
hard-snaps (causing visible position blips), but the animation cycle
|
||||
stays at whatever it was last set to (Run sticks; Walk sticks).
|
||||
|
||||
Observation matrix:
|
||||
|
||||
| Observer | Actor | Cycle Run↔Walk | Z on slopes |
|
||||
|---|---|---|---|
|
||||
| Retail | Retail | ✓ | ✓ |
|
||||
| Retail | Acdream | ✓ | ✓ |
|
||||
| Acdream | Acdream | ✓ | ✗ (only with env-var path) |
|
||||
| Acdream | Retail | ✗ | ✗ |
|
||||
|
||||
**Root cause / status:**
|
||||
|
||||
ACE only broadcasts a fresh `UpdateMotion` (UM) when the wire's
|
||||
`ForwardCommand` byte changes — i.e. on direction-key state changes
|
||||
(W press, W release). Toggling Shift while W is held changes
|
||||
`ForwardSpeed` and `HoldKey` but NOT `ForwardCommand`, so ACE does
|
||||
NOT broadcast a UM for the demote/promote. The speed change DOES
|
||||
propagate via `UpdatePosition` (position-delta velocity changes
|
||||
between Run-pace and Walk-pace), confirmed via `[VEL_DIAG]`
|
||||
serverSpeed varying ~2.5 m/s (walk) ↔ ~9 m/s (run).
|
||||
|
||||
Retail's inbound code uses UP-derived velocity to refine the visible
|
||||
cycle when no UM tells it. Acdream has the equivalent function —
|
||||
`ApplyServerControlledVelocityCycle` in `GameWindow.cs:3274` — but
|
||||
it's gated `if (IsPlayerGuid(serverGuid)) return;` for player
|
||||
remotes, exactly the case where the gap matters.
|
||||
|
||||
(Earlier hypothesized as H2 in the 2026-05-03 four-agent investigation
|
||||
but marked refuted because the [UPCYCLE] diag never fired — that
|
||||
was BECAUSE of the gate; un-gating reveals it firing per UP, which
|
||||
is the correct behavior.)
|
||||
|
||||
**Fix sketch (~10 lines):** un-gate `ApplyServerControlledVelocityCycle`
|
||||
for player remotes when `currentMotion` is a locomotion cycle
|
||||
(Run/Walk/Sidestep/Backward). UMs still drive direction-key changes
|
||||
authoritatively; UP-derived velocity refines the speed bucket within
|
||||
the same direction. Add a `LastUMUpdateTime` grace window (e.g.
|
||||
500ms) so UMs win when fresh.
|
||||
|
||||
**Files:**
|
||||
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:3274` — `ApplyServerControlledVelocityCycle`
|
||||
(the gate `if (IsPlayerGuid(serverGuid)) return;` to remove with conditions)
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:3640-3660` — call site (already
|
||||
passes through with HasServerVelocity from synthesized UP-deltas)
|
||||
- `src/AcDream.Core/Physics/ServerControlledLocomotion.cs:54-76` —
|
||||
`PlanFromVelocity` thresholds (may need re-tuning if banding is observed)
|
||||
|
||||
**Research:**
|
||||
|
||||
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md` —
|
||||
full background of the four-agent investigation
|
||||
- This session's diagnostic logs at `tools/diag-logs/walkrun-A1b-*.log`
|
||||
(UM_RAW, FWD_WIRE, SETCYCLE traces) confirming ACE's wire pattern
|
||||
|
||||
**Acceptance:**
|
||||
|
||||
- Observer in acdream watching a retail-driven character toggle Shift
|
||||
while holding W: visible leg cycle switches Run↔Walk within ~200ms
|
||||
of the wire change.
|
||||
- No regression on the working cases (acdream-on-acdream, retail
|
||||
observers, idle↔Run, idle↔Walk).
|
||||
- No spurious cycle thrashing during turning while running (ObservedOmega
|
||||
doesn't trigger velocity-bucket changes).
|
||||
|
||||
## #40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
|
||||
|
||||
**Status:** OPEN (do-not-enable; pending L.3 follow-up rebuild)
|
||||
**Severity:** N/A (gated; default behavior unaffected)
|
||||
**Filed:** 2026-05-03
|
||||
**Component:** physics / motion (per-tick remote prediction)
|
||||
|
||||
**Description:** The `ACDREAM_INTERP_MANAGER=1` per-frame remote tick
|
||||
introduced by commit `e94e791` (L.3.1+L.3.2 Task 3) is a regression and
|
||||
should not be enabled. Two visible symptoms:
|
||||
|
||||
1. **Z staircase on slopes:** observed remotes running up/down hills
|
||||
sink into rising terrain or float over receding terrain, then snap
|
||||
to correct Z at each `UpdatePosition` arrival. Body never follows
|
||||
the terrain mesh between UPs.
|
||||
|
||||
2. **Position blips during steady-state motion:** XY drifts
|
||||
unconstrained between UPs, then UP hard-snaps cause visible jumps.
|
||||
|
||||
Both symptoms ABSENT when env-var unset (default legacy path).
|
||||
|
||||
**Root cause:** the env-var path was designed to mirror retail
|
||||
`CPhysicsObj::MoveOrTeleport` (acclient @ 0x00516330). MoveOrTeleport
|
||||
is retail's network-packet entry point — minimal work. The per-frame
|
||||
physics tick is retail's `update_object` (FUN_00515020) — full chain
|
||||
including `apply_current_movement` → `UpdatePhysicsInternal` →
|
||||
`Transition::FindTransitionalPosition` (collision sweep). The legacy
|
||||
path mirrors `update_object` correctly. The env-var path stripped the
|
||||
collision sweep on a wrong assumption that this was "more retail-
|
||||
faithful" — it was the opposite.
|
||||
|
||||
Commit B (039149a, 2026-05-03) ported `ResolveWithTransition` into the
|
||||
env-var path, but the symptom persisted because the env-var path also
|
||||
clears `body.Velocity` for grounded remotes (no Euler integration of
|
||||
horizontal motion → sweep input is the catch-up offset only, which
|
||||
itself stair-steps because UPs are sampled at ~1 Hz).
|
||||
|
||||
**Files:**
|
||||
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:6042-6260` — env-var per-frame branch
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:6260+` — legacy per-frame branch (works)
|
||||
- `src/AcDream.Core/Physics/PositionManager.cs` — class itself is retail-faithful
|
||||
(port of CPositionManager::adjust_offset), only the integration was wrong
|
||||
|
||||
**Research:**
|
||||
|
||||
- This session's `2026-05-03` chronological commit log + visual verification
|
||||
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md`
|
||||
for the four-agent investigation that traced this
|
||||
|
||||
**Fix path (separate L.3 follow-up phase, NOT this session):**
|
||||
|
||||
The PositionManager class is correct retail-port. Re-integrate it as
|
||||
ADDITIVE refinement on top of the working legacy chain (small
|
||||
correction toward queued server positions, applied AFTER
|
||||
`apply_current_movement` + `UpdatePhysicsInternal` + collision sweep)
|
||||
— not as a REPLACEMENT for them. Match retail's actual `update_object`
|
||||
chain ordering: `position_manager::adjust_offset` runs after the
|
||||
primary motion + collision resolution.
|
||||
|
||||
**Acceptance:**
|
||||
|
||||
- New per-tick path enabled via env-var (or default after stabilization)
|
||||
produces the same smooth slope motion + zero blips as the legacy path.
|
||||
- Inbound `UpdatePosition` queue catch-up nudges body toward server
|
||||
authoritative position without overriding terrain Z snap or causing
|
||||
position blips.
|
||||
- Verification: side-by-side vs legacy default in 2-client setup,
|
||||
identical visible behavior.
|
||||
|
||||
## #38 — Chase camera + player feel "30 fps" since L.5 physics-tick gate
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
|
|||
|
|
@ -1,192 +0,0 @@
|
|||
# Remote-entity motion — open questions resolved via cdb live trace
|
||||
|
||||
**Date:** 2026-05-02
|
||||
**Companion to:** the three agent research reports (paste them in alongside if needed; they live in worktree `adoring-torvalds-d796cf`, `sleepy-grothendieck-9d7483`, `gracious-wright-7af984`).
|
||||
|
||||
The three reports converged on the same overall pipeline (queued
|
||||
position-chase via `InterpolationManager`) but diverged on three
|
||||
specifics. We resolved all three with a focused cdb attach to the live
|
||||
retail acclient.exe v11.4186.
|
||||
|
||||
## Resolution method
|
||||
|
||||
1. Static decomp dive into `MoveOrTeleport`, `InterpolateTo`,
|
||||
`InterpolationManager::adjust_offset`, `set_velocity`,
|
||||
`GetAutonomyBlipDistance`, `set_local_velocity`.
|
||||
2. Constant-value lookup for every named distance/velocity referenced
|
||||
by those functions (`.formats poi(acclient!NAME)`).
|
||||
3. Live trace with bps on the routing functions while the user did a
|
||||
~30-second mixed-motion scenario in retail (walk, strafe, jump,
|
||||
run-jump). Captured per-bp hit counts + `set_velocity` caller
|
||||
return addresses.
|
||||
|
||||
Scripts: `interp_discovery.cdb`, `interp_constants.cdb`,
|
||||
`interp_const2.cdb`, `interp_trace.cdb`. Logs in worktree.
|
||||
|
||||
## Question 1 — distance threshold value(s)
|
||||
|
||||
**Answer:** the agents weren't disagreeing — **they were describing two
|
||||
different thresholds doing two different jobs.**
|
||||
|
||||
| Threshold | Constant | Value | Where | Decision |
|
||||
|---|---|---:|---|---|
|
||||
| Routing gate | `MAX_PHYSICS_DISTANCE` | **96 m** | `CPhysicsObj::MoveOrTeleport` | within → InterpolateTo (queue); beyond → SetPositionSimple (slide-snap) |
|
||||
| Enqueue blip | `GetAutonomyBlipDistance()` | 100 m outdoor / 20 m indoor (creature)<br>100 m outdoor / 25 m indoor (player) | `CPhysicsObj::InterpolateTo` | beyond → enqueue InterpolationNode; within → simpler path (set_heading + StopInterpolating) |
|
||||
| Reach / duplicate-prune | `DESIRED_DISTANCE` | 0.05 m | `InterpolateTo` + `adjust_offset` | node "reached"; tail-prune duplicates |
|
||||
|
||||
The agents who said "96" cited `MAX_PHYSICS_DISTANCE` in MoveOrTeleport
|
||||
correctly. The one who said "100/25" cited `GetAutonomyBlipDistance`
|
||||
correctly but had the indoor-creature value off by 5 (it's 20, not 25 —
|
||||
**25 is the PLAYER indoor distance**, used for self-correction not for
|
||||
remote entities).
|
||||
|
||||
`GetAutonomyBlipDistance` decoded:
|
||||
|
||||
```c
|
||||
float CPhysicsObj::GetAutonomyBlipDistance(this) {
|
||||
bool isPlayer = (this == CPhysicsObj::player_object);
|
||||
bool isIndoor = (this->cell_id & 0xFFFF) >= 0x100; // 2-byte cell IDs are dungeon/inside
|
||||
if (isPlayer) {
|
||||
return isIndoor ? PLAYER_INSIDE_BLIP_DISTANCE // 25
|
||||
: PLAYER_OUTSIDE_BLIP_DISTANCE; // 100
|
||||
} else {
|
||||
return isIndoor ? CREATURE_INSIDE_BLIP_DISTANCE // 20
|
||||
: CREATURE_OUTSIDE_BLIP_DISTANCE; // 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Question 2 — polarity of the 96 m branch
|
||||
|
||||
**Answer: within 96 m → queue (InterpolateTo). Beyond 96 m → snap
|
||||
(SetPositionSimple).** Decoded from the disasm AND confirmed by trace.
|
||||
|
||||
Disasm of `CPhysicsObj::MoveOrTeleport` at +0x60:
|
||||
|
||||
```
|
||||
fld [esi+20h] ; this->distance_to_player
|
||||
fcomp MAX_PHYSICS_DISTANCE ; vs 96.0
|
||||
fnstsw ax
|
||||
test ah, 5
|
||||
jp +0x91 ; JP fires when distance > 96 → snap branch
|
||||
|
||||
; FALL-THROUGH (distance ≤ 96): queue path
|
||||
call CPhysicsObj::InterpolateTo
|
||||
|
||||
+0x91: ; (distance > 96): snap branch
|
||||
call PositionManager::StopInterpolating
|
||||
call CPhysicsObj::SetPositionSimple
|
||||
```
|
||||
|
||||
Trace results (~30 sec mixed motion in Holtburg, all entities visible
|
||||
within ~30 m of camera):
|
||||
|
||||
| BP | Hits | Notes |
|
||||
|---|---:|---|
|
||||
| `MoveOrTeleport` | 207 | every inbound UpdatePosition |
|
||||
| `InterpolateTo` | 207 | **100 % routed to queue** |
|
||||
| `SetPositionSimple` | **0** | no slide-snaps |
|
||||
| `SetPosition` | 0 | no teleports/no-cell |
|
||||
|
||||
Confirmed: every routing decision in the test went to the queue.
|
||||
SetPositionSimple is the rare exception, only used when the entity is
|
||||
beyond camera range of significance.
|
||||
|
||||
## Question 3 — does walking-remote use UpdatePosition velocity (`set_velocity`)?
|
||||
|
||||
**Answer: NO.** R3 was correct. Walking remote entities never call
|
||||
`set_velocity`; their `m_velocityVector` stays at zero (or whatever
|
||||
prior). Position progress is achieved entirely through `adjust_offset`
|
||||
walking the body toward queued waypoints.
|
||||
|
||||
Trace evidence:
|
||||
|
||||
| BP | Hits | Notes |
|
||||
|---|---:|---|
|
||||
| `set_velocity` | 7 | All 7 had **caller = `0x00511534`** |
|
||||
| `HandleVectorUpdate` (inbound 0xF74E) | 4 | jumps |
|
||||
|
||||
Resolved `0x00511534` → it's inside `CPhysicsObj::set_local_velocity`
|
||||
(starts at `0x005114d0`). And `set_local_velocity` is what retail's
|
||||
local-jump path (CMotionInterp::LeaveGround) uses to stuff the launch
|
||||
velocity into the body. So:
|
||||
|
||||
- Local player jumps 4 times → `LeaveGround → set_local_velocity →
|
||||
set_velocity` fires repeatedly (7 hits across 4 jumps; charge-frames
|
||||
+ release).
|
||||
- Inbound 0xF74E packets arrive (4) — these did NOT cause additional
|
||||
`set_velocity` hits on remote physobjs in our window. Either retail
|
||||
gates inside `DoVectorUpdate` based on entity type, or the velocity
|
||||
field got applied via a different path that doesn't trip our bp.
|
||||
- 207 walking-remote `UpdatePosition`s → **zero `set_velocity` hits**.
|
||||
|
||||
So **for walking remotes, `m_velocityVector` is zero and the
|
||||
`UpdatePhysicsInternal` Euler integration `position += velocity*dt`
|
||||
contributes nothing.** All visible motion is from `adjust_offset`
|
||||
walking the body toward queue head.
|
||||
|
||||
## Bonus — the rest of the constants we needed
|
||||
|
||||
Resolved while we were in there:
|
||||
|
||||
| Constant | Value | Where used |
|
||||
|---|---:|---|
|
||||
| `MAX_INTERPOLATED_VELOCITY_MOD` | **2.0** | `adjust_offset` — multiplier on `minterp->get_adjusted_max_speed()` (the catch-up gain) |
|
||||
| `MAX_INTERPOLATED_VELOCITY` | 7.5 m/s | `adjust_offset` fallback when minterp is unavailable |
|
||||
| `MIN_DISTANCE_TO_REACH_POSITION` | 0.20 m | `adjust_offset` per-5-frame progress threshold |
|
||||
| `max_velocity` | 50 m/s | `set_velocity` magnitude clamp |
|
||||
| `BIG_DISTANCE` | 999999 m | sentinel |
|
||||
| `CAMERA_MAP_DISTANCE` | 450 m | unrelated; map render only |
|
||||
|
||||
Constraint distances (used by the constraint sub-system, not the queue):
|
||||
|
||||
| Constant | Value |
|
||||
|---|---:|
|
||||
| `PLAYER_OUTSIDE_CONSTRAINT_DISTANCE_START` | 10 |
|
||||
| `PLAYER_OUTSIDE_CONSTRAINT_DISTANCE_MAX` | 50 |
|
||||
| `PLAYER_INSIDE_CONSTRAINT_DISTANCE_START` | 5 |
|
||||
| `PLAYER_INSIDE_CONSTRAINT_DISTANCE_MAX` | 20 |
|
||||
| `CREATURE_OUTSIDE_CONSTRAINT_DISTANCE_START` | 10 |
|
||||
| `CREATURE_OUTSIDE_CONSTRAINT_DISTANCE_MAX` | 50 |
|
||||
| `CREATURE_INSIDE_CONSTRAINT_DISTANCE_START` | 5 |
|
||||
| `CREATURE_INSIDE_CONSTRAINT_DISTANCE_MAX` | 20 |
|
||||
|
||||
## Where this leaves the implementation plan
|
||||
|
||||
The picture is now fully drawn. To match retail, acdream needs to:
|
||||
|
||||
1. **Implement `InterpolationManager`** — FIFO queue (cap 20),
|
||||
`InterpolateTo(targetPosition, isMovingTo)` enqueues with
|
||||
GetAutonomyBlipDistance + DESIRED_DISTANCE prune, `adjust_offset(dt)`
|
||||
per-tick walks toward head at `min(minterp.get_adjusted_max_speed() ×
|
||||
2, MAX_INTERPOLATED_VELOCITY_FALLBACK 7.5) × dt`,
|
||||
`NodeCompleted` pops on arrival within `DESIRED_DISTANCE 0.05`,
|
||||
`UseTime` periodic stall detection (every 5 frames; if progress < 30 %
|
||||
of expected → fail counter; > 3 fails → blip-to head).
|
||||
2. **Implement `MoveOrTeleport` routing** in the inbound UpdatePosition
|
||||
handler. Replace acdream's current hard-snap with:
|
||||
- Stale-sequence (instance/position) → ignore.
|
||||
- Teleport-sequence newer or no-cell → SetPosition (hard-snap).
|
||||
- has_contact false → no-op.
|
||||
- has_contact true && distance ≤ 96 → InterpolateTo.
|
||||
- has_contact true && distance > 96 → SetPositionSimple slide-snap.
|
||||
3. **Drop velocity-based dead-reckoning for walking remotes.** Remote
|
||||
`m_velocityVector` should stay at zero unless an inbound 0xF74E
|
||||
VectorUpdate sets it. The body's progress comes from `adjust_offset`,
|
||||
not from Euler integration of state-derived velocity.
|
||||
4. **Apply VectorUpdate.Omega.** Currently parsed but not applied — fix
|
||||
to make jumping/turning remote arcs match.
|
||||
|
||||
acdream code that goes away when this lands:
|
||||
- `RemoteMotion.SnapResidualDecayRate` and the soft-snap residual blend.
|
||||
- The locally-recomputed velocity drive between UpdatePosition packets
|
||||
(`apply_current_movement → get_state_velocity → Euler` path on remote
|
||||
entities).
|
||||
|
||||
## Files
|
||||
|
||||
- `interp_discovery.cdb` / `.log` — symbol resolution + prologues
|
||||
- `interp_constants.cdb` / `.log` — first constant lookup
|
||||
- `interp_const2.cdb` / `.log` — remaining constant lookup
|
||||
- `interp_trace.cdb` / `.log` — live routing distribution + set_velocity callers
|
||||
- This doc consolidates the answers
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# Remote-entity animation-cycle bug — investigation prompt
|
||||
|
||||
**Hand-off date:** 2026-05-03
|
||||
**Status:** open. Multiple shipped fixes today reduced the remote-entity motion problem to a single residual symptom — the **leg-cycle on observed remotes does not visibly switch between Walk / Run / Ready** even though every signal says it should. Plus minor blippiness in steady motion.
|
||||
|
||||
This document is a self-contained briefing for an agent (or fresh session) picking this up.
|
||||
|
||||
---
|
||||
|
||||
## What problem are we trying to solve?
|
||||
|
||||
When acdream observes another player driven by a parallel **retail** acclient.exe (connected to the same local ACE server), the remote character's **leg animation cycle** does not visibly change when that retail player switches between Run / Walk / Idle. The remote's **body** moves at the right speed (translation works), but the **legs keep playing whatever cycle was active before**.
|
||||
|
||||
User test: drive `+Acdream` (or any retail char) through `Press W (run) → release → Press shift+W (walk) → release` while observing in acdream's window. The body moves correctly but the leg cycle stays in idle pose / walk pose / whatever it was.
|
||||
|
||||
User-confirmed working perspectives:
|
||||
- Local +Acdream's transitions in acdream **work** ✓
|
||||
- +Acdream observed FROM a parallel retail client **work** ✓ (proves our outbound is fine)
|
||||
|
||||
So the bug is **specifically** in how acdream renders the visual cycle for an observed remote-driven character.
|
||||
|
||||
---
|
||||
|
||||
## What we shipped today (commits in chronological order)
|
||||
|
||||
```
|
||||
0997f96 fix(motion): landing fallback + TurnLeft omega sign + vel diagnostic (L.3.2)
|
||||
9960ce3 fix(motion): preserve signed TurnSpeed for remote turn animations
|
||||
842dfcd fix(motion): retail-faithful per-frame remote tick (L.3.2 follow-up)
|
||||
b1d8e12 research(motion): cdb live trace of retail walk-to-run transition
|
||||
a45c21e fix(motion): retail-faithful remote tick — clear body.Velocity, drive via seqVel
|
||||
c06b6c5 fix(motion): full queue reset on locomotion-cycle direct transitions [partly reverted]
|
||||
a2ae2ae revert: AnimationSequencer locomotion-cycle full-reset and link-skip
|
||||
357dcc0 fix(motion): SetCycle forces _currNode onto first newly-enqueued node;
|
||||
skip SubState commands in UM Commands list iteration
|
||||
```
|
||||
|
||||
**User-confirmed wins from the above:**
|
||||
- Body translation no longer races (was 2× server pace; now matches)
|
||||
- Run-in-circles smooth (rectangle-effect gone — body rotates properly between UPs)
|
||||
- Jump landing position correct (no mid-air force-land)
|
||||
- Jump landing animation works (Falling → Ready visible)
|
||||
- Turn-left visibly turns left (was animating right with snap-back)
|
||||
- Signed TurnSpeed preserved (ACE encodes TurnLeft as `TurnCommand=TurnRight, Speed=negative`)
|
||||
|
||||
**User-confirmed remaining bugs:**
|
||||
1. **Walk↔Run leg cycle on observed remotes does not visibly switch.** Body advances at correct new speed but legs continue playing previous cycle.
|
||||
2. **Residual small "blip" corrections during steady-state motion** (run, walk, strafe). User describes this as a periodic micro-jitter — small but visible.
|
||||
3. **(Possible)** ~20% steady-state walk overshoot (`maxSeqSpeed=3.120, serverSpeed≈2.6`) per VEL_DIAG measurements — not yet root-caused. May or may not be related to (2).
|
||||
|
||||
---
|
||||
|
||||
## What we proved about bug 1 (the cycle-doesn't-switch)
|
||||
|
||||
Per the diagnostic infrastructure built today:
|
||||
|
||||
| Signal | Result |
|
||||
|---|---|
|
||||
| `[FWD_WIRE]` — wire-arrival ForwardCommand transitions | ✅ ACE delivers `WalkForward → RunForward` (and direct walk↔run) correctly |
|
||||
| `[CMD_LIST]` — Commands list at receive time | Empty for walk/run UMs; contains Ready/Action class for some others |
|
||||
| `[HASCYCLE]` — does the dat have the requested cycle | ✅ True for both `0x44000007` (Run) and `0x45000005` (Walk) on style `0x8000003D` (NonCombat Humanoid) |
|
||||
| `[SETCYCLE]` — animCycle picker calls into AnimationSequencer.SetCycle | ✅ Fires with correct (style, motion, speed) |
|
||||
| `[SEQSTATE]` — per-tick `ae.Sequencer.CurrentMotion` for the observed remote | ✅ Holds the new motion correctly (e.g. shows `0x44000007 speed=2.939` after Run press, then `0x41000003 speed=1.000` after release) |
|
||||
|
||||
So:
|
||||
- ACE wire data is correct.
|
||||
- Our parser updates `InterpretedState` correctly.
|
||||
- `OnLiveMotionUpdated` calls `SetCycle` with correct args.
|
||||
- `SetCycle` updates the sequencer's `CurrentMotion` correctly.
|
||||
- The cycle data the sequencer would play exists in the dat.
|
||||
|
||||
**But the visible leg cycle does NOT update.** Therefore the bug is **downstream of `ae.Sequencer.CurrentMotion`** — somewhere between the sequencer's internal state and the rendered MeshRefs:
|
||||
- `AnimationSequencer.Advance(dt)` returning frames from the wrong node
|
||||
- `BuildBlendedFrame()` reading from a stale `_currNode`
|
||||
- `_currNode` advancing through stale link/head frames before reaching the new cycle
|
||||
- Or how the per-part transforms returned by Advance get applied to the entity's `MeshRefs` for remote entities
|
||||
|
||||
We attempted a fix in `357dcc0` that forces `_currNode` onto the first newly-enqueued node in SetCycle — user reports **no visible change** after this fix.
|
||||
|
||||
---
|
||||
|
||||
## What's different between local (works) and remote (doesn't)
|
||||
|
||||
Both call **the same `AnimationSequencer.SetCycle` method** in `src/AcDream.Core/Physics/AnimationSequencer.cs:360`. So the sequencer code itself is shared.
|
||||
|
||||
Local +Acdream path:
|
||||
- `PlayerMovementController` → `UpdatePlayerAnimation` (in `GameWindow.cs:6664`) → resolves cycle → `ae.Sequencer.SetCycle(...)`
|
||||
- Fast-path early-return when cmd+speed unchanged (line 6713-6714)
|
||||
- `OnLiveMotionUpdated` skips wire-echo SetCycle for the local player guid (line 2707)
|
||||
|
||||
Remote (observed retail char) path:
|
||||
- Wire arrives → `OnLiveMotionUpdated` (`GameWindow.cs:3203`)
|
||||
- "animCycle picker" at line 2842-2867 chooses the cycle based on Forward / Sidestep / Turn priority
|
||||
- HasCycle fallback chain at line 2939
|
||||
- `ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed)` at line 2988
|
||||
- Then iterates `update.MotionState.Commands` and routes each through `AnimationCommandRouter` (357dcc0 made this skip SubState class)
|
||||
- Then ALSO updates `remoteMot.Motion.InterpretedState.ForwardCommand/ForwardSpeed` for body.Velocity computation
|
||||
- Then ALSO calls `remoteMot.Motion.DoInterpretedMotion(...)` for sidestep/turn axes
|
||||
|
||||
**Hypotheses to investigate:**
|
||||
|
||||
A) After `SetCycle` fires, some other call in `OnLiveMotionUpdated` re-cycles the sequencer back. We've eliminated the `Commands` list (357dcc0 skip-SubState). Other candidates: `PlayAction` calls inside `RouteWireCommand`, the spawn-time SetCycle at line 2313, or something in `ApplyServerControlledVelocityCycle` (line 3238).
|
||||
|
||||
B) `_currNode` actually IS in the right place after SetCycle but `Advance(dt)` doesn't read from it correctly. Maybe a thread-safety issue (SetCycle on net thread, Advance on render thread, partial state visible).
|
||||
|
||||
C) `Advance` returns the right frames but `seqFrames` are not applied to the entity's `MeshRefs` for the remote entity specifically. Look at `GameWindow.cs:6510-6589` — the per-part transform application loop. There's no obvious local-vs-remote branch but worth tracing.
|
||||
|
||||
D) The MeshRefs themselves get rebuilt each frame and the rebuild reads from a different source for remotes. The `newMeshRefs` list is built per-frame at line 6567.
|
||||
|
||||
E) Local player's `ae.Sequencer.SetCycle` is called at a higher rate than remote's (per-input vs per-UM). Maybe the queue stays cleaner with frequent calls, and the bug is exposed only when SetCycle is sparse.
|
||||
|
||||
F) **Most likely** based on what we've seen: `Advance` plays through stale link frames before reaching the cycle. Our 357dcc0 fix forces `_currNode` onto the first newly-enqueued node — but for `Ready→Run`, the newly-enqueued sequence is `[Ready→Run link, Run cycle]`. `_currNode` lands on the **link**, the link plays for ~0.5–1 second, then the run cycle starts. User perceives the link's "transition pose" as "still walking / still idle."
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic infrastructure available
|
||||
|
||||
All env-var gated on `ACDREAM_REMOTE_VEL_DIAG=1`:
|
||||
|
||||
| Diag | Where | What it shows |
|
||||
|---|---|---|
|
||||
| `[FWD_WIRE]` | `GameWindow.cs:2793-2800` | Each ForwardCommand transition received per remote |
|
||||
| `[CMD_LIST]` | `GameWindow.cs:3119-3133` | Commands list contents at UM receive time |
|
||||
| `[HASCYCLE]` | `GameWindow.cs:2939-2947` | HasCycle result for the requested cycle |
|
||||
| `[SETCYCLE]` | `GameWindow.cs:2972-2986` | Each animCycle picker → SetCycle call |
|
||||
| `[SEQSTATE]` | `GameWindow.cs:6520-6532` | Per-tick `ae.Sequencer.CurrentMotion` (1Hz throttled) |
|
||||
| `[TURN_WIRE]` | `GameWindow.cs:3050-3057` | TurnCommand wire arrivals with signed speed |
|
||||
| `[OMEGA_DIAG]` | `GameWindow.cs:5901-5912` | Per-tick omega being applied to body |
|
||||
| `[VEL_DIAG]` | `GameWindow.cs:3327-3343` | Server-broadcast speed vs maxSeqSpeed per UP |
|
||||
|
||||
Also gated on `ACDREAM_INTERP_MANAGER=1` is the entire retail-faithful per-tick remote motion path. Set both env vars when reproducing.
|
||||
|
||||
The repo has `tools/cdb-scripts/` set up for live tracing of retail acclient.exe via cdb.exe. Two trace scripts already proven working:
|
||||
- `walk_run_motion_trace.cdb` + `walk_run_motion_trace.log` — captured the exact retail walk→run sequence and proved retail uses `MotionTableManager::add_to_queue` without `truncate_animation_list`.
|
||||
|
||||
To launch retail tracing: have user start retail and connect, then in PowerShell:
|
||||
```
|
||||
& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" `
|
||||
-pn acclient.exe -cf "tools\cdb-scripts\walk_run_motion_trace.cdb" *>&1 |
|
||||
Tee-Object -FilePath "tools\cdb-scripts\walk_run_motion_trace.log.console"
|
||||
```
|
||||
Auto-detaches at 200 hits via `.detach` (do NOT use `qd` per CLAUDE.md gotcha — silently ignored). NEVER `Stop-Process` cdb — takes retail down with it.
|
||||
|
||||
---
|
||||
|
||||
## What retail actually does (from cdb live trace)
|
||||
|
||||
For a walk→run direct transition retail's call sequence is:
|
||||
|
||||
```
|
||||
[79] CPhysicsObj::DoInterpretedMotion: motion=45000005 walk start (shift+W)
|
||||
[82] CMotionTable::DoObjectMotion: motion=45000005
|
||||
[83] MotionTableManager::add_to_queue: arg1=45000005 arg2=00000001 ← walk added looping
|
||||
|
||||
[89] CPhysicsObj::DoInterpretedMotion: motion=44000007 run start (release shift)
|
||||
[92] CMotionTable::DoObjectMotion: motion=44000007
|
||||
[93] MotionTableManager::add_to_queue: arg1=44000007 arg2=00000001 ← run added looping
|
||||
|
||||
[104] CMotionTable::StopObjectMotion: motion=44000007 run end (release W)
|
||||
```
|
||||
|
||||
`MotionTableManager::truncate_animation_list` was on bp the entire trace and **never fired**. Retail just appends new motions to the queue and lets `MotionTableManager::CheckForCompletedMotions` (`0x0051BE00`) and `MotionTableManager::remove_redundant_links` (`0x0051BF20`) handle the natural progression — neither of which we have ported.
|
||||
|
||||
This suggests our `AnimationSequencer.SetCycle` rebuild semantics (ClearCyclicTail + enqueue link + enqueue cycle) is fundamentally different from retail's "append-only" `MotionTableManager`. May not matter for visual output as long as our queue manipulations land in the same end state, but it's a structural mismatch worth exploring if the tactical fixes don't pan out.
|
||||
|
||||
---
|
||||
|
||||
## File locations
|
||||
|
||||
- **`src/AcDream.Core/Physics/AnimationSequencer.cs`** — SetCycle (line 360), Advance (690), BuildBlendedFrame (1254), ClearCyclicTail (1117), AdvanceToNextAnimation (1150), EnqueueMotionData (1101), LoadAnimNode (1037)
|
||||
- **`src/AcDream.App/Rendering/GameWindow.cs`** — OnLiveMotionUpdated (3203), TickAnimations (5851), animCycle picker block (2842-2988), the seqFrames-to-MeshRefs application loop (6510-6635), UpdatePlayerAnimation (6664)
|
||||
- **`src/AcDream.Core/Physics/AnimationCommandRouter.cs`** — RouteWireCommand (53), Classify (29)
|
||||
- **`src/AcDream.Core/Physics/MotionInterpreter.cs`** — get_state_velocity (587), GetMaxSpeed (968), apply_current_movement (653), HitGround (924)
|
||||
- **`src/AcDream.Core/Physics/PositionManager.cs`** — ComputeOffset (37) (the per-tick combiner)
|
||||
- **`src/AcDream.Core/Physics/InterpolationManager.cs`** — Enqueue, AdjustOffset (224), stall detection
|
||||
- **Reference decomp:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (1.4M-line pseudo-C with full PDB names)
|
||||
- **Symbols index:** `docs/research/named-retail/symbols.json` (greppable name → address)
|
||||
- **Verbatim retail headers:** `docs/research/named-retail/acclient.h` (struct field offsets)
|
||||
|
||||
---
|
||||
|
||||
## Concrete next steps for the bug
|
||||
|
||||
1. **Add a per-tick diag that prints `_currNode.Anim.Id` + `_framePosition` for the observed remote.** This will conclusively answer whether `_currNode` is on the new cycle, on a stale link, or somewhere else. Implement near the existing SEQSTATE diag in `GameWindow.cs:6520`. Ask user to do the precise test sequence (W only, then shift+W only, no turns/no mouse) and read the log.
|
||||
|
||||
2. **Add a diag that prints `seqFrames[0].Origin` and `seqFrames[0].Orientation`** (the result of Advance) before applying to MeshRefs. If the values change meaningfully between cycles → bug is in MeshRefs application. If they're stuck → bug is in Advance/BuildBlendedFrame.
|
||||
|
||||
3. **Compare the call ORDER of SetCycle for local vs remote.** Maybe local's UpdatePlayerAnimation calls SetCycle then immediately also re-resolves cycle data and passes it through. Or local has frame-resolution state we lack for remotes.
|
||||
|
||||
4. **Try the retail-faithful additive `add_to_queue` semantics:** modify SetCycle to skip ClearCyclicTail and just append new motion data. The `MotionTableManager::CheckForCompletedMotions` cleanup we don't port might be needed — but a primitive version (drop nodes whose `IsLooping=true` count exceeds 1, keeping the newest) might suffice as a starting point.
|
||||
|
||||
5. **Trace retail's CSequence::update / update_internal calls live** with cdb to see what frames ARE returned per tick for a remote running and transitioning. We have the cdb toolchain set up; pattern existing scripts in `tools/cdb-scripts/`.
|
||||
|
||||
6. **If all else fails, dispatch a research agent** with the prompt below.
|
||||
|
||||
---
|
||||
|
||||
## For the next research agent — exact assignment
|
||||
|
||||
> Read this entire document.
|
||||
>
|
||||
> Read `src/AcDream.Core/Physics/AnimationSequencer.cs` end-to-end, focusing on:
|
||||
> - `SetCycle` (line 360-560) — what state it mutates and in what order
|
||||
> - `Advance` (line 690-784) — how it consumes the queue and what it returns
|
||||
> - `BuildBlendedFrame` (line 1254-1313) — how the visible per-part transforms are computed
|
||||
> - `ClearCyclicTail` (line 1117-1140) and `AdvanceToNextAnimation` (line 1150-1166) — node lifecycle in the queue
|
||||
>
|
||||
> Then read `src/AcDream.App/Rendering/GameWindow.cs:5851-6635` — the `TickAnimations` method including the dead-reckoning blocks, sequencer Advance call, and the seqFrames-to-MeshRefs application loop.
|
||||
>
|
||||
> Answer:
|
||||
>
|
||||
> 1. After `SetCycle` is called for `RunForward` (with `linkData != null` and `cycleData != null`), what is the precise queue state, the value of `_currNode`, and the value of `_framePosition` immediately after SetCycle returns? Trace step by step including ClearCyclicTail's effect on `_currNode`. Cite line numbers.
|
||||
>
|
||||
> 2. On the next render tick when `Advance(dt=0.0167)` is called, what does it do? Specifically, does it advance through the link frames first, or skip them, or play them and stop at the cycle? What pose does `BuildBlendedFrame` return at the end?
|
||||
>
|
||||
> 3. Is there any code path between `SetCycle` returning and the next `Advance` call that could RESET `_currNode` back to a stale node? List every SetCycle call site (there are ~12 in GameWindow.cs) and identify any that fire on the per-tick path (not just on UM receive).
|
||||
>
|
||||
> 4. Is there any difference in how `seqFrames` is consumed for the local player vs a remote-observed entity in the loop at lines 6566-6635? Both use `if (seqFrames is not null) { origin = seqFrames[i].Origin; ... }`. Find any conditional branch that bypasses seqFrames for remotes.
|
||||
>
|
||||
> Output: a concise (<800 word) report with line citations and a clear hypothesis for the root cause of the visible-cycle-doesn't-switch bug. Do NOT modify any code.
|
||||
|
||||
---
|
||||
|
||||
## Quick reproduction recipe
|
||||
|
||||
1. Start local ACE server (user has this running on `127.0.0.1:9000`).
|
||||
2. Start a parallel **retail** acclient.exe and connect with a different character (NOT `+Acdream`).
|
||||
3. Build acdream: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||
4. Launch acdream from the main repo dir with both env vars:
|
||||
```powershell
|
||||
$env:ACDREAM_INTERP_MANAGER = "1"
|
||||
$env:ACDREAM_REMOTE_VEL_DIAG = "1"
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = "1"
|
||||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||
$env:ACDREAM_TEST_PORT = "9000"
|
||||
$env:ACDREAM_TEST_USER = "testaccount"
|
||||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||
Tee-Object -FilePath launch.log
|
||||
```
|
||||
5. From the retail client, drive the test character: stand 2s, press W (run) 4s, release, press shift+W (walk) 4s, release.
|
||||
6. Observe the test character in the acdream window. Bug: leg cycle does NOT visibly switch between idle / run / walk poses.
|
||||
7. Read diags from `launch.log` (UTF-16 — use `Get-Content -Encoding Unicode`).
|
||||
|
||||
---
|
||||
|
||||
## Notes on what NOT to do
|
||||
|
||||
- **Do not pass `skipTransitionLink: true` unconditionally to SetCycle** — tried in commit `c06b6c5` (link skip), broke landing-from-jump, sit-down, and every other transition that needs its dat link to play. Reverted in `a2ae2ae`.
|
||||
- **Do not full-reset the queue on every motion change** — same commit, also reverted. Side effect: removed end-animations everywhere.
|
||||
- **Do not "scale body.Velocity by observed serverSpeed/predictedSpeed"** — tried during the day, user explicitly rejected as a hack. Always use predicted velocity from `get_state_velocity` (= `RunAnimSpeed × ForwardSpeed`).
|
||||
- **Do not `Stop-Process cdb`** while it's attached to retail — takes retail down with it (CLAUDE.md). Use `.detach` inside bp actions for graceful exit.
|
||||
|
|
@ -1,815 +0,0 @@
|
|||
# Phase L.3.1 — InterpolationManager Core + MoveOrTeleport Routing — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
>
|
||||
> **Heavy subagent use is the user's explicit request.** Tasks marked **`[PARALLEL-A]`**, **`[PARALLEL-B]`**, etc. can be dispatched simultaneously as concurrent Sonnet subagents and the parent reviews each return before integrating. The plan flags every parallelization opportunity.
|
||||
|
||||
**Goal:** Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (`InterpolationManager` + `MoveOrTeleport` routing). Apply parsed-but-ignored `VectorUpdate.Omega`. Tear out the now-redundant `RemoteMotion` soft-snap residual code. Ship behind `ACDREAM_INTERP_MANAGER=1` env-var gate, then collapse the dual paths after visual verification.
|
||||
|
||||
**Architecture:** New pure-data `InterpolationManager` class (FIFO queue cap 20 + `AdjustOffset(dt, maxSpeed) → Vector3` per-frame catch-up) composed into the existing per-remote `RemoteMotion` container. Inbound `0xF748` UpdatePosition handler (`OnLivePositionUpdated`) replaced by retail-faithful router (stale-seq → ignore; teleport-seq newer → snap; within 96 m → enqueue; beyond 96 m → slide-snap). Per-frame remote tick adds `Interp.AdjustOffset(dt) → body.Position`. Single-keyword env-var rollback during dev; cleanup commit after sign-off.
|
||||
|
||||
**Tech Stack:** C# / .NET 10 / xUnit. Edits in `AcDream.App` + `AcDream.Core`. No new NuGet deps. Tests at `tests/AcDream.Core.Tests/Physics/*Tests.cs`.
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../specs/2026-05-02-l3-remote-entity-motion-design.md) (committed 08cb7f9).
|
||||
|
||||
**Research baseline:** [`docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md`](../../research/2026-05-02-remote-entity-motion/resolved-via-cdb.md).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/AcDream.Core/Physics/InterpolationManager.cs` | **CREATE** | Pure-data FIFO position-queue + adjust_offset math. No game/window deps. Composed into RemoteMotion. |
|
||||
| `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs` | **CREATE** | ~13 unit tests covering queue mechanics, AdjustOffset math, stall detection. |
|
||||
| `src/AcDream.Core/Physics/MotionInterpreter.cs` | **MODIFY** | Add public `GetMaxSpeed()` returning motion-table-derived max for current InterpretedState. |
|
||||
| `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` | **MODIFY** | Add ~3 tests covering GetMaxSpeed for Walk/Run/Idle. |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) RemoteMotion class gains `Interp` field. (b) `OnLivePositionUpdated` env-var gated routing. (c) Per-frame remote tick env-var gated `Interp.AdjustOffset` add. (d) `OnLiveVectorUpdated` applies `Omega` to body. |
|
||||
| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** | Insert Phase L.3 entry between L.2 and M. |
|
||||
| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual-path branches; delete old hard-snap path; delete RemoteMotion soft-snap residual fields. |
|
||||
|
||||
---
|
||||
|
||||
## Open Precision Item
|
||||
|
||||
The spec flags one ambiguity: **does retail's `InterpolationManager::UseTime` (acclient @ 0x00555F20) blip-to-HEAD or blip-to-TAIL on stall?** The two agent reports disagreed. Default for the initial port = HEAD. Task 0 below resolves this via a 30-second cdb static decomp dive (no live attach needed).
|
||||
|
||||
---
|
||||
|
||||
## Task Decomposition Overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Task 0 [PARALLEL-A] │
|
||||
│ Resolve UseTime head/tail │
|
||||
│ (decomp read, ~5 min) │
|
||||
└──────────────────────────────┘
|
||||
┌──────────────────────────────┐
|
||||
│ Task 1 [PARALLEL-B] │
|
||||
┌─ DISPATCH 3 SUBAGENTS IN PARALLEL ──────────────►│ InterpolationManager + tests│
|
||||
│ └──────────────────────────────┘
|
||||
│ ┌──────────────────────────────┐
|
||||
│ │ Task 2 [PARALLEL-C] │
|
||||
│ │ MotionInterpreter.GetMaxSpeed│
|
||||
│ └──────────────────────────────┘
|
||||
|
||||
┌─ AFTER 0+1+2 LAND ────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Task 3 — RemoteMotion.Interp field (sequential, single edit) │
|
||||
│ ↓ │
|
||||
│ Task 4 — OnLivePositionUpdated env-var routing (sequential) │
|
||||
│ ↓ │
|
||||
│ Task 5 — Per-frame remote tick env-var Interp.AdjustOffset (sequential) │
|
||||
│ ↓ │
|
||||
│ Task 6 — OnLiveVectorUpdated.Omega (sequential, 3 lines) │
|
||||
│ │
|
||||
│ Task 7 — Visual verification (USER GATE) │
|
||||
│ ↓ user signs off │
|
||||
│ │
|
||||
│ ┌─ DISPATCH 2 SUBAGENTS IN PARALLEL ──────┐ │
|
||||
│ │ Task 8: Cleanup commit │ │
|
||||
│ │ Task 9: Roadmap update │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 0 — [PARALLEL-A] Resolve `UseTime` head-vs-tail via static decomp
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Read-only; no code changes.
|
||||
|
||||
**Files:**
|
||||
- Read: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (search for `InterpolationManager::UseTime` near line ~352261-353375)
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> Read the named retail decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt`. Find `InterpolationManager::UseTime` (search by exact string `InterpolationManager::UseTime`). It should appear around line 352261-353375. Read the body of that function (~100 lines).
|
||||
>
|
||||
> The function decides what to do when the per-5-frame stall counter shows the entity isn't catching up to its queued waypoints (`node_fail_counter > 3`). The two prior research agents disagreed on whether the resulting "blip" snaps the body to the HEAD of the queue (the next intended waypoint) or to the TAIL (the most recent server-sent position).
|
||||
>
|
||||
> Report under 200 words: which is it (HEAD or TAIL), with the line range from the decomp that proves it. If the decompile is ambiguous (e.g. comparison polarity artifact), flag that and recommend a default. No code edits.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 0.1: Dispatch the subagent**
|
||||
|
||||
Use the Agent tool with `subagent_type=general-purpose`, model `sonnet`, prompt above.
|
||||
|
||||
- [ ] **Step 0.2: Read subagent report; record decision in implementation note**
|
||||
|
||||
Append a one-line note to the InterpolationManager source comment (created in Task 1) recording the resolution.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — [PARALLEL-B] InterpolationManager class + ~13 unit tests
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Tasks 0 + 2.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/AcDream.Core/Physics/InterpolationManager.cs`
|
||||
- Create: `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs`
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Phase L.3.1 Task 1. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` sections "L.3.1 architecture" → "New file" through the unit-test list. Read the research at `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` for the constants table.
|
||||
>
|
||||
> Create the file `src/AcDream.Core/Physics/InterpolationManager.cs` matching the spec's API:
|
||||
> ```csharp
|
||||
> public sealed class InterpolationManager {
|
||||
> void Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo);
|
||||
> Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp);
|
||||
> bool IsActive { get; }
|
||||
> void Clear();
|
||||
> // constants from spec
|
||||
> }
|
||||
> ```
|
||||
> The spec uses retail's `Position` type in the signature, but acdream's PhysicsBody uses `Vector3 Position` separately from `uint CellId`. So:
|
||||
> - `Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo)` — caller is responsible for resolving cell deltas
|
||||
> - `AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)` returns the world-space delta to add to body.Position this frame
|
||||
>
|
||||
> Implement the spec's `AdjustOffset` algorithm exactly (steps 1-9 as written). For the stall-blip branch, use HEAD as the default (Task 0 may override this; Task 0's report should be available — if it says TAIL, use TAIL). Use `LinkedList<InterpolationNode>` for the queue.
|
||||
>
|
||||
> Create `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs` with the 13 tests listed in the spec under "L.3.1 unit tests" → "Queue mechanics", "AdjustOffset math", "Stall detection". Use xUnit. Match the test-file pattern of existing files (e.g. `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs`): top-level `using` block, `namespace AcDream.Core.Tests.Physics;`, then test methods. Use `file sealed class` for any test-only helpers.
|
||||
>
|
||||
> Build with `cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo` and `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager"`. Both must be green.
|
||||
>
|
||||
> Commit with `feat(physics): InterpolationManager core (L.3.1 Task 1)` and Co-Authored-By Claude Opus 4.7. Direct-to-main per CLAUDE.md.
|
||||
>
|
||||
> Report under 300 words: what you built, test results, any deviations from the spec (if you had to deviate, justify).
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 1.1: Dispatch the subagent in parallel with Tasks 0 and 2**
|
||||
|
||||
Use the Agent tool with `subagent_type=general-purpose`, `model=sonnet`. Send all 3 dispatch calls in a single message so they run concurrently.
|
||||
|
||||
- [ ] **Step 1.2: Verify subagent's commit**
|
||||
|
||||
```bash
|
||||
git log -1 --stat src/AcDream.Core/Physics/InterpolationManager.cs
|
||||
```
|
||||
|
||||
Expected: commit message starts with `feat(physics): InterpolationManager core (L.3.1 Task 1)`. Files changed include `InterpolationManager.cs` + `InterpolationManagerTests.cs`.
|
||||
|
||||
- [ ] **Step 1.3: Re-run tests in parent session to confirm green**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager"
|
||||
```
|
||||
|
||||
Expected: all ~13 tests pass.
|
||||
|
||||
- [ ] **Step 1.4: Spot-check the implementation file**
|
||||
|
||||
Read the created file. Verify: API surface matches spec exactly; constants are public consts with the spec's values; `AdjustOffset` algorithm follows spec steps 1-9; stall-blip uses HEAD (or TAIL per Task 0 outcome).
|
||||
|
||||
If anything diverges materially from the spec without justification in the subagent's report, dispatch a fix subagent. If the deviation is minor and harmless, accept it.
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — [PARALLEL-C] `MotionInterpreter.GetMaxSpeed()` + ~3 unit tests
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Tasks 0 + 1.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/MotionInterpreter.cs` (add one public method, ~10-15 lines)
|
||||
- Modify: `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` (add ~3 tests)
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Phase L.3.1 Task 2. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.1 architecture" → "Modified — `MotionInterpreter`".
|
||||
>
|
||||
> Add a public method `GetMaxSpeed()` to `src/AcDream.Core/Physics/MotionInterpreter.cs`. It must port retail's `CMotionInterp::get_max_speed` semantics: return the motion-table-derived max speed for the current `InterpretedState.ForwardCommand`. Acdream's `MotionInterpreter` already knows the constants `RunAnimSpeed = 4.0f` and `WalkAnimSpeed = 3.12f` (search the file for these). Approximate retail logic:
|
||||
> ```csharp
|
||||
> public float GetMaxSpeed() {
|
||||
> return InterpretedState.ForwardCommand switch {
|
||||
> MotionCommand.RunForward => RunAnimSpeed * (WeenieObj?.InqRunRate(out var r) == true ? r : MyRunRate),
|
||||
> MotionCommand.WalkForward => WalkAnimSpeed,
|
||||
> MotionCommand.WalkBackward => WalkAnimSpeed * 0.65f, // BackwardsFactor
|
||||
> _ => 0f, // idle / non-locomotion
|
||||
> };
|
||||
> }
|
||||
> ```
|
||||
> If retail decomp suggests a different formula, prefer that — search the named decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt` for `CMotionInterp::get_max_speed` (around line 305235-305280). Report what you found.
|
||||
>
|
||||
> Add ~3 unit tests to `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs`:
|
||||
> - `GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRate`
|
||||
> - `GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed`
|
||||
> - `GetMaxSpeed_Idle_ReturnsZero`
|
||||
>
|
||||
> Use the existing `FakeWeenie` test helper from MotionInterpreterTests.cs.
|
||||
>
|
||||
> Build + test:
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo
|
||||
> dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"
|
||||
> ```
|
||||
> Both green.
|
||||
>
|
||||
> Commit with `feat(physics): MotionInterpreter.GetMaxSpeed for InterpolationManager (L.3.1 Task 2)` and Co-Authored-By Claude Opus 4.7. Direct-to-main.
|
||||
>
|
||||
> Report under 200 words: what the formula is, decomp reference if found, test results.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 2.1: Dispatch in parallel with Tasks 0 and 1** (single message, three concurrent Agent tool calls)
|
||||
|
||||
- [ ] **Step 2.2: Verify subagent's commit**
|
||||
|
||||
```bash
|
||||
git log -1 --stat src/AcDream.Core/Physics/MotionInterpreter.cs
|
||||
```
|
||||
|
||||
Expected: commit message `feat(physics): MotionInterpreter.GetMaxSpeed (L.3.1 Task 2)`.
|
||||
|
||||
- [ ] **Step 2.3: Re-run tests**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"
|
||||
```
|
||||
|
||||
Expected: existing tests + new ~3 tests all pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Add `Interp` field to `RemoteMotion` class
|
||||
|
||||
**Owner:** Parent (you). Tiny mechanical edit; not worth a subagent.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~224 — `RemoteMotion` class)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 3.1: Read the RemoteMotion class definition**
|
||||
|
||||
```bash
|
||||
grep -n "private sealed class RemoteMotion" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
|
||||
```
|
||||
|
||||
Then Read tool from the line returned, ~120 lines.
|
||||
|
||||
- [ ] **Step 3.2: Add `Interp` field**
|
||||
|
||||
In the `RemoteMotion` class body (after the existing field declarations, before the constructor `public RemoteMotion()`), add:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Per-remote position-waypoint queue + catch-up math (retail's
|
||||
/// InterpolationManager). Replaces the hard-snap-then-Euler-extrapolate
|
||||
/// path when ACDREAM_INTERP_MANAGER=1 — see L.3.1 spec.
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
|
||||
new AcDream.Core.Physics.InterpolationManager();
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: Build and verify**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
```
|
||||
|
||||
Expected: 0 warnings, 0 errors, "Build succeeded."
|
||||
|
||||
- [ ] **Step 3.4: Run all tests**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: existing tests still pass (no behavior change yet).
|
||||
|
||||
- [ ] **Step 3.5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(motion): RemoteMotion gains InterpolationManager field (L.3.1 Task 3)
|
||||
|
||||
Composes the new InterpolationManager (Task 1) into the per-remote
|
||||
container. Field exists but is not consumed yet — Tasks 4 and 5 wire
|
||||
it into the routing + per-frame tick.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Env-var gated routing in `OnLivePositionUpdated`
|
||||
|
||||
**Owner:** Parent. Manual edit because the surrounding handler is complex (~70 lines) and we need to wrap it without disrupting the legacy path.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~3151 — `OnLivePositionUpdated`)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 4.1: Read the entire OnLivePositionUpdated method to understand current structure**
|
||||
|
||||
```bash
|
||||
grep -n "OnLivePositionUpdated\b" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
|
||||
```
|
||||
|
||||
Then Read tool from `OnLivePositionUpdated` start, ~100 lines (until the next method declaration).
|
||||
|
||||
Note: the method currently does (a) lazy-create RemoteMotion if not in dict, (b) hard-snap `body.Position` and `body.Orientation`, (c) update RemoteMotion.SnapResidualDecayRate / soft-snap residual fields, (d) clear airborne / set Z-fields if has-velocity changed.
|
||||
|
||||
- [ ] **Step 4.2: Locate the specific point where the hard-snap happens**
|
||||
|
||||
Look for `rm.Body.Position = ...` (or `body.Position = ...`) inside this handler. Mark its surrounding context.
|
||||
|
||||
- [ ] **Step 4.3: Wrap the snap block in env-var conditional**
|
||||
|
||||
Pseudocode of the change:
|
||||
|
||||
```csharp
|
||||
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
// ... existing lazy-create + parse + identification (unchanged) ...
|
||||
if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) {
|
||||
rm = new RemoteMotion();
|
||||
_remoteDeadReckon[update.Guid] = rm;
|
||||
}
|
||||
var targetPos = ...; // existing extraction (Vector3)
|
||||
var targetOri = ...; // existing extraction (Quaternion)
|
||||
|
||||
// NEW: env-var gated retail-faithful routing (L.3.1)
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
// CPhysicsObj::MoveOrTeleport router (acclient @ 0x00516330):
|
||||
// - stale-seq: ignore (TODO: implement IsStaleSequence wrapping uint16 compare on the four sequence counters; for now allow all to land)
|
||||
// - teleport-seq newer or no-cell: SetPosition (hard-snap)
|
||||
// - has_contact false: no-op
|
||||
// - has_contact true && distance ≤ 96: Interp.Enqueue
|
||||
// - has_contact true && distance > 96: SetPositionSimple (slide-snap)
|
||||
|
||||
// Distance source: retail uses entity->[+0x20] (entity-to-local-player).
|
||||
// Acdream computes equivalent via local player position.
|
||||
Vector3 localPlayerPos = _playerController?.Position ?? Vector3.Zero;
|
||||
float dist = Vector3.Distance(targetPos, localPlayerPos);
|
||||
|
||||
bool teleportFlag = false; // TODO: source from update sequence comparison once IsStaleSequence is in
|
||||
bool hasContact = update.Position.HasContact; // verify field name in CreateObject.ServerPosition
|
||||
|
||||
if (teleportFlag) {
|
||||
rm.Body.Position = targetPos;
|
||||
rm.Body.Orientation = targetOri;
|
||||
rm.Interp.Clear();
|
||||
}
|
||||
else if (!hasContact) {
|
||||
// no-op
|
||||
}
|
||||
else if (dist > 96f) {
|
||||
rm.Interp.Clear();
|
||||
rm.Body.Position = targetPos;
|
||||
rm.Body.Orientation = targetOri;
|
||||
}
|
||||
else {
|
||||
float headingFromQuat = ExtractYawFromQuaternion(targetOri); // see helper below
|
||||
rm.Interp.Enqueue(targetPos, headingFromQuat, isMovingTo: false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// EXISTING hard-snap path (unchanged) — kept until cleanup commit (Task 8)
|
||||
rm.Body.Position = targetPos;
|
||||
rm.Body.Orientation = targetOri;
|
||||
// ... rest of existing soft-snap + residual fields ...
|
||||
}
|
||||
|
||||
// Helper (place near OnLivePositionUpdated):
|
||||
private static float ExtractYawFromQuaternion(Quaternion q)
|
||||
{
|
||||
// Inverse of YawToAcQuaternion: extract Z-axis rotation angle.
|
||||
// Acdream's player Yaw convention: Yaw=0 faces +X.
|
||||
return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y), 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
||||
}
|
||||
```
|
||||
|
||||
If `update.Position.HasContact` doesn't exist, look for the equivalent on `CreateObject.ServerPosition` — likely `update.Position.IsGrounded` or similar based on parsed PositionPack flags. Use whatever's there; acceptable to file a TODO to plumb it through if it's not currently parsed.
|
||||
|
||||
- [ ] **Step 4.4: Build**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
```
|
||||
|
||||
Expected: 0 errors, 0 warnings.
|
||||
|
||||
- [ ] **Step 4.5: Run tests**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: all existing tests pass (env-var off by default → existing behavior unchanged).
|
||||
|
||||
- [ ] **Step 4.6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(motion): MoveOrTeleport routing in OnLivePositionUpdated (L.3.1 Task 4)
|
||||
|
||||
Wraps the hard-snap path in ACDREAM_INTERP_MANAGER=1 env-var guard.
|
||||
When set, runs retail-faithful routing (acclient!CPhysicsObj::
|
||||
MoveOrTeleport @ 0x00516330): teleport-seq → SetPosition; within 96m
|
||||
→ Interp.Enqueue; beyond 96m → SetPositionSimple slide-snap.
|
||||
|
||||
Existing hard-snap behavior preserved when flag is unset (default).
|
||||
Old path will be removed in cleanup commit after visual verification.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — Env-var gated per-frame `Interp.AdjustOffset` add
|
||||
|
||||
**Owner:** Parent. Touches the per-frame remote tick (~line 5680-5760).
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (per-frame remote tick block)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 5.1: Locate the remote tick block**
|
||||
|
||||
```bash
|
||||
grep -n "_remoteDeadReckon.TryGetValue.*serverGuid" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
|
||||
```
|
||||
|
||||
Look for the line ~5689 entry; read 80 lines forward to see the whole tick block (where `apply_current_movement` and `body.UpdatePhysicsInternal` are called).
|
||||
|
||||
- [ ] **Step 5.2: Wrap the legacy tick body in an if/else against the env-var**
|
||||
|
||||
Pseudocode of the change:
|
||||
|
||||
```csharp
|
||||
if (ae.Sequencer is not null
|
||||
&& serverGuid != 0
|
||||
&& serverGuid != _playerServerGuid
|
||||
&& _remoteDeadReckon.TryGetValue(serverGuid, out var rm))
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
// NEW PATH: queued position-chase via InterpolationManager.
|
||||
// Walking remotes have m_velocityVector == 0 in retail; all visible
|
||||
// motion comes from adjust_offset walking the body toward queue head
|
||||
// at 2 × motion_max_speed × dt.
|
||||
if (rm.Interp.IsActive)
|
||||
{
|
||||
float maxSpeed = rm.Motion.GetMaxSpeed(); // Task 2 method
|
||||
Vector3 delta = rm.Interp.AdjustOffset((float)dt, rm.Body.Position, maxSpeed);
|
||||
rm.Body.Position += delta;
|
||||
}
|
||||
// For airborne remotes, OnLiveVectorUpdated has set body.Velocity;
|
||||
// body.UpdatePhysicsInternal below applies gravity. No queue
|
||||
// adjustment competes with the arc.
|
||||
rm.Body.UpdatePhysicsInternal((float)dt);
|
||||
}
|
||||
else
|
||||
{
|
||||
// EXISTING hard-snap + Euler path (unchanged) — kept until cleanup
|
||||
if (!rm.Airborne) {
|
||||
// ... existing apply_current_movement, force-OnWalkable, etc.
|
||||
}
|
||||
rm.Body.UpdatePhysicsInternal((float)dt);
|
||||
// ... existing post-physics processing ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The exact shape depends on the existing code — preserve everything in the `else` branch verbatim. The `if` branch is the new one.
|
||||
|
||||
- [ ] **Step 5.3: Build**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
```
|
||||
|
||||
Expected: 0 errors.
|
||||
|
||||
- [ ] **Step 5.4: Run tests**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: all pass (flag off → existing behavior).
|
||||
|
||||
- [ ] **Step 5.5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(motion): per-frame Interp.AdjustOffset in remote tick (L.3.1 Task 5)
|
||||
|
||||
When ACDREAM_INTERP_MANAGER=1, the per-frame remote tick uses
|
||||
InterpolationManager.AdjustOffset to walk body.Position toward the
|
||||
queue head at 2 × motion-max-speed × dt (retail's
|
||||
acclient!InterpolationManager::adjust_offset @ 0x00555D30).
|
||||
|
||||
Legacy apply_current_movement + Euler dead-reckoning preserved when
|
||||
flag is unset.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6 — Apply `VectorUpdate.Omega` in `OnLiveVectorUpdated`
|
||||
|
||||
**Owner:** Parent. Tiny edit, no env-var gate (this is a strict bug-fix that improves both old and new paths).
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~3064 — `OnLiveVectorUpdated`)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 6.1: Read the existing handler**
|
||||
|
||||
```bash
|
||||
grep -n "OnLiveVectorUpdated" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
|
||||
```
|
||||
|
||||
Read from line returned, ~30 lines.
|
||||
|
||||
- [ ] **Step 6.2: Find the velocity-application line and add omega next to it**
|
||||
|
||||
Find:
|
||||
```csharp
|
||||
if (update.Velocity is { } v)
|
||||
rm.Body.Velocity = v;
|
||||
```
|
||||
|
||||
Add immediately after:
|
||||
```csharp
|
||||
if (update.Omega is { } w)
|
||||
rm.Body.Omega = w;
|
||||
```
|
||||
|
||||
Verify the field name on `VectorUpdate.Parsed` — it might be `Omega` or `AngularVelocity`. If it's not present at all, that's a parser gap — file as a follow-up issue and skip this task. Most likely it's already parsed because the spec confirmed "currently parsed-but-ignored".
|
||||
|
||||
- [ ] **Step 6.3: Build + test**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: green.
|
||||
|
||||
- [ ] **Step 6.4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix(motion): apply VectorUpdate.Omega to remote body (L.3.1 Task 6)
|
||||
|
||||
VectorUpdate.Omega was parsed by WorldSession but never written to
|
||||
the remote body's Omega field, leaving remote jumping/turning
|
||||
arcs flat. Apply it alongside the existing Velocity assignment.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7 — Visual verification (USER GATE)
|
||||
|
||||
**Owner:** User. Cannot be automated.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 7.1: Kill any running acdream**
|
||||
|
||||
```powershell
|
||||
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 6
|
||||
```
|
||||
|
||||
- [ ] **Step 7.2: Launch acdream with ACDREAM_INTERP_MANAGER=1**
|
||||
|
||||
```powershell
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = "1"
|
||||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||
$env:ACDREAM_TEST_PORT = "9000"
|
||||
$env:ACDREAM_TEST_USER = "testaccount"
|
||||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||
$env:ACDREAM_INTERP_MANAGER = "1"
|
||||
dotnet run --project C:\Users\erikn\source\repos\acdream\src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "C:\Users\erikn\source\repos\acdream\.claude\worktrees\jovial-blackburn-773942\launch.log"
|
||||
```
|
||||
|
||||
- [ ] **Step 7.3: User performs the visual test matrix**
|
||||
|
||||
Have a parallel retail observer toon watching `+Acdream`. On the retail observer side:
|
||||
|
||||
1. Walk forward 5 sec
|
||||
2. Walk backward 5 sec
|
||||
3. Strafe left/right 5 sec each
|
||||
4. Stop
|
||||
5. Run forward 5 sec
|
||||
6. Jump from standstill 2-3x
|
||||
7. Jump while running 2-3x
|
||||
8. Turn quickly while running
|
||||
|
||||
For each, verify (against the acceptance criteria in the spec):
|
||||
- Walking remotes glide smoothly (no 1-Hz popping)
|
||||
- Backward / strafe / turn behaviors from commit 17a9ff1 still work
|
||||
- Jump arcs are curved (Omega applied)
|
||||
|
||||
- [ ] **Step 7.4: User signs off OR files a regression**
|
||||
|
||||
If anything regresses, file the specifics and either fix forward (parent dispatches a focused-fix subagent) or revert the env-var to legacy mode while debugging.
|
||||
|
||||
If everything looks right, proceed to Tasks 8 + 9 (parallel cleanup + roadmap).
|
||||
|
||||
---
|
||||
|
||||
## Task 8 — [PARALLEL-D] Cleanup commit
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Task 9.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths in 4 + 5; delete RemoteMotion soft-snap residual fields)
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Phase L.3.1 Task 8: cleanup. The user has visually verified that ACDREAM_INTERP_MANAGER=1 works correctly. Now collapse the dual-path scaffolding into a single retail-faithful path.
|
||||
>
|
||||
> In `src/AcDream.App/Rendering/GameWindow.cs`:
|
||||
>
|
||||
> 1. **In `OnLivePositionUpdated`** (added in Task 4): delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")` wrapper. Keep ONLY the routing block inside it (the new path). Delete the legacy hard-snap fall-through.
|
||||
>
|
||||
> 2. **In the per-frame remote tick block** (modified in Task 5): same — delete the `if/else` env-var gate. Keep ONLY the new path (Interp.AdjustOffset). Delete the legacy `apply_current_movement` + force-OnWalkable + Euler-extrapolate code in the `else` branch.
|
||||
>
|
||||
> 3. **In the `RemoteMotion` class** (~line 224): delete `SnapResidualDecayRate` and any soft-snap residual fields (search for `_snapResidual`, `SnapResidualDecayRate`, `SoftSnap`, etc.). Also delete any related code in `OnLivePositionUpdated` and the per-frame tick that touched those fields (it should already be gone if Task 4/5 wrapped them in the env-var gate, but double-check).
|
||||
>
|
||||
> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain.
|
||||
>
|
||||
> Build: `cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo`. 0 warnings, 0 errors.
|
||||
> Test: `dotnet test --no-build --nologo`. All green.
|
||||
>
|
||||
> Commit:
|
||||
> ```
|
||||
> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead soft-snap path (L.3.1 Task 8)
|
||||
>
|
||||
> User has visually verified the new InterpolationManager-based remote
|
||||
> motion (commits f2 + f5 + f6 from L.3.1). Collapses the env-var
|
||||
> dual-path: deletes legacy hard-snap + Euler-extrapolate code from
|
||||
> OnLivePositionUpdated and the per-frame remote tick, deletes the
|
||||
> SnapResidualDecayRate + soft-snap residual fields from RemoteMotion.
|
||||
>
|
||||
> Net diff: ~50 lines deletion. Single retail-faithful path remains.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> ```
|
||||
>
|
||||
> Report under 200 words: line counts deleted, files touched, test results.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 8.1: Dispatch the subagent in parallel with Task 9**
|
||||
|
||||
Use Agent tool, `general-purpose`, `sonnet`. Send simultaneously with Task 9's dispatch.
|
||||
|
||||
- [ ] **Step 8.2: Verify the commit landed and the diff is sensible**
|
||||
|
||||
```bash
|
||||
git log -1 --stat
|
||||
git show HEAD -- src/AcDream.App/Rendering/GameWindow.cs | head -100
|
||||
```
|
||||
|
||||
Expected: ~50 lines deleted, no `ACDREAM_INTERP_MANAGER` in the diff (only its removal).
|
||||
|
||||
- [ ] **Step 8.3: Re-run all tests in parent session**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
|
||||
```
|
||||
|
||||
Expected: green.
|
||||
|
||||
- [ ] **Step 8.4: Confirm zero env-var references remain**
|
||||
|
||||
```bash
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
|
||||
```
|
||||
|
||||
Expected: no output.
|
||||
|
||||
---
|
||||
|
||||
## Task 9 — [PARALLEL-D] Roadmap update
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Task 8.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-04-11-roadmap.md`
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Phase L.3.1 Task 9: add the Phase L.3 entry to the roadmap.
|
||||
>
|
||||
> Read `docs/plans/2026-04-11-roadmap.md` to understand the existing format. Find the spot between `### Phase L.2 — Movement & Collision Conformance` and `### Phase M — Network Stack Conformance` (search for those exact headings).
|
||||
>
|
||||
> Insert a new Phase L.3 entry with this content:
|
||||
> ```markdown
|
||||
> ### Phase L.3 — Remote Entity Motion Conformance
|
||||
>
|
||||
> **Status:** L.3.1 IN PROGRESS / SHIPPED (depending on whether cleanup commit has landed when you read this).
|
||||
>
|
||||
> **Goal:** Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (`InterpolationManager` + `MoveOrTeleport` routing). Apply parsed-but-ignored `VectorUpdate.Omega`. Drop the parallel soft-snap residual scaffolding `RemoteMotion` was carrying.
|
||||
>
|
||||
> **Why now:** Live cdb traces (2026-05-02) confirmed retail uses a per-physobj FIFO position queue with `adjust_offset(dt)` walking the body at 2× motion-table-max-speed toward the head, NOT velocity-based dead-reckoning. acdream's chop comes from the wrong algorithm category, not just bad parameters.
|
||||
>
|
||||
> **Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md).
|
||||
>
|
||||
> **Plan (L.3.1):** [`docs/superpowers/plans/2026-05-02-l3-1-interpolation-manager.md`](../superpowers/plans/2026-05-02-l3-1-interpolation-manager.md).
|
||||
>
|
||||
> **Sub-lanes:**
|
||||
>
|
||||
> - **L.3.1 — InterpolationManager core + routing.** New `InterpolationManager` class, `MoveOrTeleport` routing replacing the hard-snap, `VectorUpdate.Omega` application, deletion of `RemoteMotion` soft-snap residual.
|
||||
> - **L.3.2 — PositionManager.** Combines per-frame animation root-motion offset with the InterpolationManager's catch-up offset before writing the body's frame. Mirrors retail `CPhysicsObj::UpdateObjectInternal`. Spec to be drafted after L.3.1 ships.
|
||||
> - **L.3.3 — MoveToManager.** Replaces `RemoteMoveToDriver` MVP with full retail-faithful port: retracking, sticky-to-target, fail-distance progress checks, sphere-cylinder distance variants. Spec to be drafted after L.3.2 ships.
|
||||
> ```
|
||||
>
|
||||
> Also update the file's top-line `**Status:** Living document. Updated YYYY-MM-DD for ...` line — change the date to today (2026-05-02) and the trailing reason to `for Phase L.3 remote-entity motion planning`.
|
||||
>
|
||||
> Build (no code changed but verify nothing broke):
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
> ```
|
||||
>
|
||||
> Commit:
|
||||
> ```
|
||||
> docs(roadmap): Phase L.3 — Remote Entity Motion Conformance (L.3.1 Task 9)
|
||||
>
|
||||
> Adds the Phase L.3 entry between L.2 (collision) and M (network).
|
||||
> Lists the three sub-lanes (L.3.1 in progress, L.3.2 + L.3.3 sketched).
|
||||
> Cross-references the design spec and the L.3.1 implementation plan.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> ```
|
||||
>
|
||||
> Report under 100 words: that the entry is inserted, location, build green.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 9.1: Dispatch in parallel with Task 8** (single message)
|
||||
|
||||
- [ ] **Step 9.2: Verify the commit**
|
||||
|
||||
```bash
|
||||
git log -1 --stat docs/plans/2026-04-11-roadmap.md
|
||||
```
|
||||
|
||||
Expected: commit message `docs(roadmap): Phase L.3 — Remote Entity Motion Conformance`. File diff shows the new section in the right location.
|
||||
|
||||
- [ ] **Step 9.3: Optional final push**
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git push origin main
|
||||
```
|
||||
|
||||
(Per CLAUDE.md, ask user before pushing.)
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
End-to-end smoke test after L.3.1 fully lands (post-Task 9):
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream
|
||||
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo # green
|
||||
dotnet test --no-build --nologo # all green (~110 tests)
|
||||
git log --oneline -10 # see L.3.1 commits in order
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed)
|
||||
grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted)
|
||||
```
|
||||
|
||||
Then user re-runs the visual test matrix from Task 7.3 with no env-var set (default behavior is now the new path).
|
||||
|
||||
If everything's green: L.3.1 done. Brainstorm L.3.2 next (PositionManager).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes (for the executor)
|
||||
|
||||
- **Task 4's `IsStaleSequence`** is intentionally deferred — the legacy code doesn't check sequences either. Filing a follow-up TODO is acceptable; not a blocker for L.3.1.
|
||||
- **Task 4's `update.Position.HasContact`** field name is a guess — verify against `CreateObject.ServerPosition` definition. If absent, file a parser-gap follow-up; for L.3.1 default to `hasContact = true` (allow all to enqueue).
|
||||
- **Task 5's `dt` source** — the existing per-frame block already has `dt` from the render loop or computes it from `nowSec - lastTime`. Reuse whatever's there.
|
||||
- **Task 6's `update.Omega`** field — verify against `VectorUpdate.Parsed`. If named `AngularVelocity` use that.
|
||||
- **Subagent failure handling:** if a subagent reports a deviation that breaks the spec contract, dispatch a fix subagent or take it over manually. Don't let a confused subagent leave broken code in main.
|
||||
|
|
@ -1,785 +0,0 @@
|
|||
# Phase L.3.1+L.3.2 Combined — PositionManager + Retail-Faithful Remote Tick
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add the PositionManager combiner (animation root motion + InterpolationManager corrections) that was originally deferred to L.3.2, plumb `IsGrounded` through `EntityPositionUpdate`, and rewrite the per-frame remote tick + `OnLivePositionUpdated` env-var-on branches to match retail's `MoveOrTeleport` semantics. This eliminates the 1-Hz chop and endless-jump bugs surfaced during Task 7 visual verification.
|
||||
|
||||
**Architecture:** Pure-data `PositionManager.ComputeOffset(dt, body.Position, seqVel, ori, interp, maxSpeed) → Vector3` returns the per-frame world-space delta to add to body.Position. Combines (a) animation root motion = `seqVel * dt` rotated by body orientation with (b) `InterpolationManager.AdjustOffset` correction. Per-frame tick always runs all steps (matches retail `UpdateObjectInternal`). `OnLivePositionUpdated` routes per `MoveOrTeleport`: airborne → no-op; landing transition → snap + clear flags; grounded → enqueue or slide-snap. Server is authoritative for airborne arcs (no local prediction fights gravity).
|
||||
|
||||
**Tech Stack:** C# / .NET 10 / xUnit. No new NuGet deps. Tests at `tests/AcDream.Core.Tests/Physics/*Tests.cs`.
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../specs/2026-05-02-l3-remote-entity-motion-design.md) (committed `c4446e7`).
|
||||
|
||||
**Already shipped (do NOT rebuild):**
|
||||
- `f43f168` + `927636e` Task 1 — InterpolationManager
|
||||
- `9c5634a` + `5b26d28` Task 2 — MotionInterpreter.GetMaxSpeed
|
||||
- `517a3ce` Task 3 — RemoteMotion.Interp field
|
||||
- `062e19f` Task 4 — OnLivePositionUpdated env-var routing v1
|
||||
- `ae79e34` Task 5 — Per-frame Interp.AdjustOffset v1
|
||||
- `e08accf` Task 6 — VectorUpdate.Omega
|
||||
- `1641d6e` revert of band-aids
|
||||
- `c4446e7` spec revision
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/AcDream.Core/Physics/PositionManager.cs` | **CREATE** | Pure-function combiner: animation root motion + Interp correction. ~50 lines including XML docs. |
|
||||
| `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` | **CREATE** | 6 unit tests against pure `ComputeOffset`. |
|
||||
| `src/AcDream.Core.Net/Messages/UpdatePosition.cs` | **MODIFY** | Add `IsGrounded` to `Parsed` record, populate from `flags & PositionFlags.IsGrounded`. ~3 lines. |
|
||||
| `src/AcDream.Core.Net/WorldSession.cs` | **MODIFY** | Add `IsGrounded` to `EntityPositionUpdate` record, pass through in PositionUpdated invoke. ~2 lines. |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) `RemoteMotion` gains `Position` field; (b) rewrite `OnLivePositionUpdated` env-var-on branch (airborne no-op + landing transition + grounded routing); (c) rewrite `TickAnimations` env-var-on branch (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). |
|
||||
| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual paths; delete `RemoteMotion` soft-snap residual fields. |
|
||||
| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** (cleanup phase) | Update Phase L.3 entry to reflect L.3.1+L.3.2 combined. |
|
||||
| `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` | **MODIFY** (cleanup phase) | Mark L.3.1+L.3.2 as SHIPPED. |
|
||||
|
||||
---
|
||||
|
||||
## Task Decomposition Overview
|
||||
|
||||
```
|
||||
Task 1 — PositionManager class + 6 tests (subagent)
|
||||
↓
|
||||
Task 2 — Plumb IsGrounded through EntityPositionUpdate (parent, 2 files, ~5 lines)
|
||||
↓
|
||||
Task 3 — Retail-faithful per-frame remote tick (subagent — biggest change)
|
||||
↓
|
||||
Task 4 — USER GATE: visual verification with retail observer
|
||||
↓ (after sign-off)
|
||||
┌─ DISPATCH IN PARALLEL ──────────────────┐
|
||||
│ Task 5: Cleanup commit (subagent) │
|
||||
│ Task 6: Roadmap + spec status (parent) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — PositionManager class + 6 unit tests
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose).
|
||||
|
||||
**Files:**
|
||||
- Create: `src/AcDream.Core/Physics/PositionManager.cs`
|
||||
- Create: `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs`
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Task 1 of Phase L.3.1+L.3.2 in the acdream codebase. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.2 architecture" → "New file — `src/AcDream.Core/Physics/PositionManager.cs`".
|
||||
>
|
||||
> **What to build:**
|
||||
>
|
||||
> Create `src/AcDream.Core/Physics/PositionManager.cs`:
|
||||
>
|
||||
> ```csharp
|
||||
> using System.Numerics;
|
||||
>
|
||||
> namespace AcDream.Core.Physics;
|
||||
>
|
||||
> /// <summary>
|
||||
> /// Per-frame combiner for remote-entity motion: animation root motion
|
||||
> /// + InterpolationManager catch-up correction. Pure function — no
|
||||
> /// side effects, no hidden state.
|
||||
> ///
|
||||
> /// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730):
|
||||
> /// rootOffset = CPartArray::Update(dt) // animation
|
||||
> /// PositionManager::adjust_offset(rootOffset) // adds correction
|
||||
> /// frame.origin += rootOffset
|
||||
> ///
|
||||
> /// In acdream the animation root motion is sourced from
|
||||
> /// AnimationSequencer.CurrentVelocity (body-local velocity from the
|
||||
> /// active locomotion cycle). We rotate that by the body's orientation
|
||||
> /// to get a world-space delta, then add the InterpolationManager's
|
||||
> /// world-space correction.
|
||||
> /// </summary>
|
||||
> public sealed class PositionManager
|
||||
> {
|
||||
> /// <summary>
|
||||
> /// Compute the per-frame world-space delta to add to body.Position.
|
||||
> /// </summary>
|
||||
> /// <param name="dt">Per-frame delta time, seconds.</param>
|
||||
> /// <param name="currentBodyPosition">Body's current world-space position.</param>
|
||||
> /// <param name="seqVel">
|
||||
> /// Body-local velocity from the active animation cycle
|
||||
> /// (from <c>AnimationSequencer.CurrentVelocity</c>); pass
|
||||
> /// <c>Vector3.Zero</c> if the entity has no sequencer or is on a
|
||||
> /// non-locomotion cycle.
|
||||
> /// </param>
|
||||
> /// <param name="ori">Body orientation; used to rotate seqVel from body-local to world.</param>
|
||||
> /// <param name="interp">The remote's InterpolationManager (for AdjustOffset call).</param>
|
||||
> /// <param name="maxSpeed">From <c>MotionInterpreter.GetMaxSpeed()</c> — passed to AdjustOffset for the catch-up clamp.</param>
|
||||
> public Vector3 ComputeOffset(
|
||||
> double dt,
|
||||
> Vector3 currentBodyPosition,
|
||||
> Vector3 seqVel,
|
||||
> Quaternion ori,
|
||||
> InterpolationManager interp,
|
||||
> float maxSpeed)
|
||||
> {
|
||||
> // Step 1: animation root motion (body-local → world).
|
||||
> Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
> Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
|
||||
>
|
||||
> // Step 2: interpolation correction (world-space already).
|
||||
> Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
|
||||
>
|
||||
> // Step 3: combined delta.
|
||||
> return rootMotionWorld + correction;
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Create `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` with EXACTLY these 6 test names (these are the contract):
|
||||
>
|
||||
> 1. `ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion`
|
||||
> - seqVel = Vector3.Zero, no enqueued nodes in interp
|
||||
> - Assert: returned offset == Vector3.Zero
|
||||
>
|
||||
> 2. `ComputeOffset_AnimationOnly_Forward_BodyAdvances`
|
||||
> - seqVel = (0, 4, 0) (4 m/s forward), ori = Quaternion.Identity, dt = 0.1
|
||||
> - Assert: returned offset == (0, 0.4, 0) (forward 0.4m)
|
||||
>
|
||||
> 3. `ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth`
|
||||
> - seqVel = (0, 4, 0), ori = quaternion rotating +Y → -Y (180° around Z), dt = 0.1
|
||||
> - Assert: returned offset.Y ≈ -0.4 (south)
|
||||
>
|
||||
> 4. `ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue`
|
||||
> - seqVel = Vector3.Zero, interp has 1 enqueued node 1m ahead, dt = 0.1, maxSpeed = 4f
|
||||
> - Expected: AdjustOffset returns the catch-up step (≤ 1m, clamped); ComputeOffset returns same
|
||||
>
|
||||
> 5. `ComputeOffset_BothActive_Combined`
|
||||
> - seqVel = (0, 4, 0) — root motion (0, 0.4, 0)
|
||||
> - interp has node 1m ahead — AdjustOffset returns ~Vector3.UnitY * step
|
||||
> - Assert: returned offset == rootMotion + correction
|
||||
>
|
||||
> 6. `ComputeOffset_LocalToWorldRotation_Yaw90`
|
||||
> - seqVel = (0, 1, 0) (forward 1 m/s in body frame)
|
||||
> - ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f) (yaw +90°)
|
||||
> - dt = 1
|
||||
> - Verify the rotation is applied correctly. With yaw +90° around Z, body-local +Y rotates to world... compute the expected and assert with precision: 4.
|
||||
>
|
||||
> Use xUnit, `namespace AcDream.Core.Tests.Physics;`, file-private fakes via `file sealed class` if needed. Read `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` for the existing pattern.
|
||||
>
|
||||
> Note: Tests #4 and #5 need a real `InterpolationManager` (not a fake) because PositionManager calls AdjustOffset directly. Construct one inline in each test, Enqueue what you need, and call ComputeOffset.
|
||||
>
|
||||
> **Build + test:**
|
||||
>
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo
|
||||
> dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager"
|
||||
> ```
|
||||
>
|
||||
> Both green. 6 tests pass.
|
||||
>
|
||||
> **Commit:**
|
||||
>
|
||||
> ```bash
|
||||
> git add src/AcDream.Core/Physics/PositionManager.cs tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> feat(physics): PositionManager combiner class + 6 unit tests (L.3.2)
|
||||
>
|
||||
> Pure-function ComputeOffset(dt, pos, seqVel, ori, interp, maxSpeed) →
|
||||
> Vector3. Combines animation root motion (seqVel × dt rotated by body
|
||||
> orientation) with InterpolationManager.AdjustOffset world-space
|
||||
> correction. Mirrors retail CPhysicsObj::UpdateObjectInternal
|
||||
> (acclient @ 0x00513730).
|
||||
>
|
||||
> Composed into RemoteMotion in subsequent task (L.3.1+L.3.2 Task 3);
|
||||
> not yet consumed.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Self-review checklist:**
|
||||
> - [ ] `PositionManager` is public sealed class
|
||||
> - [ ] `ComputeOffset` is the only public method (no other API)
|
||||
> - [ ] All 6 tests have the exact names listed
|
||||
> - [ ] Tests #4 and #5 use a real `InterpolationManager`
|
||||
> - [ ] No game/window/sequencer dependencies — only `System.Numerics` + `AcDream.Core.Physics.InterpolationManager`
|
||||
> - [ ] Build clean, all 6 tests pass
|
||||
> - [ ] Commit references "L.3.2"
|
||||
>
|
||||
> **Report:**
|
||||
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
> - What you built (1-2 sentences)
|
||||
> - Test results (count, any deviations)
|
||||
> - Files changed
|
||||
> - Concerns (if any)
|
||||
|
||||
**Steps for the parent (controller):**
|
||||
|
||||
- [ ] **Step 1.1: Dispatch the implementer subagent** using the prompt above.
|
||||
- [ ] **Step 1.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.Core/Physics/PositionManager.cs
|
||||
```
|
||||
Expected: commit message starts with `feat(physics): PositionManager combiner class`.
|
||||
- [ ] **Step 1.3: Re-run tests in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager"
|
||||
```
|
||||
Expected: 6 tests pass.
|
||||
- [ ] **Step 1.4: Dispatch spec compliance reviewer** (use `general-purpose`, Sonnet). Verify the 6 tests have the EXACT names listed and verify `ComputeOffset` algorithm matches the spec's pseudocode.
|
||||
- [ ] **Step 1.5: Dispatch code quality reviewer** (use `superpowers:code-reviewer`). Check for: API surface (only ComputeOffset public), test quality, no superfluous deps.
|
||||
- [ ] **Step 1.6: Address review issues if any.** If issues found, dispatch fix subagent. Re-review.
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Plumb `IsGrounded` through `EntityPositionUpdate`
|
||||
|
||||
**Owner:** Parent. Mechanical edit, ~5 lines across 2 files.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:62-69` (add `IsGrounded` to `Parsed` record)
|
||||
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:166` (populate `IsGrounded` in the constructor call)
|
||||
- Modify: `src/AcDream.Core.Net/WorldSession.cs:110-113` (add `IsGrounded` to `EntityPositionUpdate` record)
|
||||
- Modify: `src/AcDream.Core.Net/WorldSession.cs:711-714` (pass `posUpdate.Value.IsGrounded` through)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 2.1: Read existing `UpdatePosition.Parsed` record + TryParse return**
|
||||
```bash
|
||||
grep -n "public readonly record struct Parsed\|return new Parsed" "C:/Users/erikn/source/repos/acdream/src/AcDream.Core.Net/Messages/UpdatePosition.cs"
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Add `IsGrounded` field to `UpdatePosition.Parsed`**
|
||||
|
||||
Edit `src/AcDream.Core.Net/Messages/UpdatePosition.cs` (~line 62):
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
uint? PlacementId,
|
||||
ushort InstanceSequence = 0,
|
||||
ushort TeleportSequence = 0,
|
||||
ushort ForcePositionSequence = 0);
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
uint? PlacementId,
|
||||
bool IsGrounded,
|
||||
ushort InstanceSequence = 0,
|
||||
ushort TeleportSequence = 0,
|
||||
ushort ForcePositionSequence = 0);
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3: Populate `IsGrounded` in the `Parsed` constructor call (~line 166)**
|
||||
|
||||
Find the line `return new Parsed(guid, serverPos, velocity, placementId,` (~line 166) and change to pass `(flags & PositionFlags.IsGrounded) != 0` as the new IsGrounded argument. Looks roughly like:
|
||||
|
||||
```csharp
|
||||
return new Parsed(guid, serverPos, velocity, placementId,
|
||||
(flags & PositionFlags.IsGrounded) != 0,
|
||||
instSeq, teleSeq, forceSeq);
|
||||
```
|
||||
|
||||
(Verify the trailing-arg layout against what's actually there; preserve any existing trailing arguments.)
|
||||
|
||||
- [ ] **Step 2.4: Add `IsGrounded` field to `WorldSession.EntityPositionUpdate`**
|
||||
|
||||
Edit `src/AcDream.Core.Net/WorldSession.cs:110`:
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
public readonly record struct EntityPositionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity);
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
public readonly record struct EntityPositionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
bool IsGrounded);
|
||||
```
|
||||
|
||||
- [ ] **Step 2.5: Pass `IsGrounded` through in PositionUpdated invoke (~line 711)**
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||
posUpdate.Value.Guid,
|
||||
posUpdate.Value.Position,
|
||||
posUpdate.Value.Velocity));
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||
posUpdate.Value.Guid,
|
||||
posUpdate.Value.Position,
|
||||
posUpdate.Value.Velocity,
|
||||
posUpdate.Value.IsGrounded));
|
||||
```
|
||||
|
||||
- [ ] **Step 2.6: Build + test**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: 0 build errors. Same 4 pre-existing test failures, no new failures.
|
||||
|
||||
- [ ] **Step 2.7: Commit**
|
||||
```bash
|
||||
git add src/AcDream.Core.Net/Messages/UpdatePosition.cs src/AcDream.Core.Net/WorldSession.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(net): plumb IsGrounded through EntityPositionUpdate (L.3.2)
|
||||
|
||||
PositionFlags.IsGrounded (0x04) was already parsed by UpdatePosition
|
||||
but not exposed through Parsed record or EntityPositionUpdate.
|
||||
Adds the bool field to both records so OnLivePositionUpdated can
|
||||
consume it for retail-faithful MoveOrTeleport routing
|
||||
(acclient @ 0x00516330: has_contact=false → no-op during airborne arc).
|
||||
|
||||
Consumed in subsequent task (L.3.1+L.3.2 Task 3).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Retail-faithful per-frame remote tick
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Largest task — touches 3 distinct sites in `GameWindow.cs`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (RemoteMotion class line ~224 + OnLivePositionUpdated env-var branch + TickAnimations env-var branch)
|
||||
|
||||
**Subagent dispatch prompt:**
|
||||
|
||||
> You are implementing Task 3 of Phase L.3.1+L.3.2 in the acdream codebase. This task rewrites two env-var-gated branches in `src/AcDream.App/Rendering/GameWindow.cs` to consume the new PositionManager (Task 1) and IsGrounded plumbing (Task 2).
|
||||
>
|
||||
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main branch — direct-to-main per CLAUDE.md.
|
||||
>
|
||||
> **Spec:** `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` "L.3.2 architecture" sections.
|
||||
>
|
||||
> **Three changes in `GameWindow.cs`:**
|
||||
>
|
||||
> ### Change 1: `RemoteMotion` class gains `Position` field
|
||||
>
|
||||
> Find the existing `Interp` field (added in commit `517a3ce`). Right after it, add:
|
||||
>
|
||||
> ```csharp
|
||||
> /// <summary>
|
||||
> /// Per-frame combiner for animation root motion + InterpolationManager
|
||||
> /// correction (Phase L.3.2). Consumed in TickAnimations to compute the
|
||||
> /// per-frame body.Position delta.
|
||||
> /// </summary>
|
||||
> public AcDream.Core.Physics.PositionManager Position { get; } =
|
||||
> new AcDream.Core.Physics.PositionManager();
|
||||
> ```
|
||||
>
|
||||
> ### Change 2: Rewrite `OnLivePositionUpdated` env-var-on branch
|
||||
>
|
||||
> Find the existing env-var-on block in `OnLivePositionUpdated` (was added at commit `062e19f`). It currently looks roughly like:
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> rmState.Body.Orientation = rot;
|
||||
> // teleport check, dist check, etc.
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Replace the env-var-on body with this new logic:
|
||||
>
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> // Orientation always snaps on receipt — the InterpolationManager
|
||||
> // walks position only; heading would otherwise lag the queue.
|
||||
> rmState.Body.Orientation = rot;
|
||||
>
|
||||
> // ── AIRBORNE NO-OP ────────────────────────────────────────────
|
||||
> // Mirrors retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
|
||||
> // when has_contact==0, return false (don't touch body, don't queue).
|
||||
> // body.Velocity (set once by OnLiveVectorUpdated at jump start) keeps
|
||||
> // integrating gravity via per-frame UpdatePhysicsInternal. Server is
|
||||
> // authoritative for the arc; we don't predict it locally.
|
||||
> if (!update.IsGrounded)
|
||||
> return;
|
||||
>
|
||||
> // ── LANDING TRANSITION ─────────────────────────────────────────
|
||||
> // First IsGrounded=true UP after rmState.Airborne signals landed.
|
||||
> // Clear airborne flags, hard-snap to authoritative landing position,
|
||||
> // clear interpolation queue (any pre-jump waypoints are stale).
|
||||
> if (rmState.Airborne)
|
||||
> {
|
||||
> rmState.Airborne = false;
|
||||
> rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
> rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
> rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
> | AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
> rmState.Interp.Clear();
|
||||
> rmState.Body.Position = worldPos;
|
||||
> return;
|
||||
> }
|
||||
>
|
||||
> // ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
|
||||
> const float MaxPhysicsDistance = 96f;
|
||||
> var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||
> float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos);
|
||||
>
|
||||
> if (dist > MaxPhysicsDistance)
|
||||
> {
|
||||
> // Beyond view bubble: SetPositionSimple slide-snap. Clear queue.
|
||||
> rmState.Interp.Clear();
|
||||
> rmState.Body.Position = worldPos;
|
||||
> }
|
||||
> else
|
||||
> {
|
||||
> // Within view bubble: enqueue waypoint for adjust_offset to walk to.
|
||||
> // PositionManager (called per-frame in TickAnimations) handles the
|
||||
> // actual body advancement — mix of animation root motion + queue
|
||||
> // correction.
|
||||
> float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||
> rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
||||
> }
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> The legacy `else` branch (env-var unset) STAYS UNCHANGED.
|
||||
>
|
||||
> If `ExtractYawFromQuaternion` doesn't exist anymore (it might have been removed in the revert), re-add it near the original location (search for it in commit `062e19f`'s diff). The body is:
|
||||
> ```csharp
|
||||
> private static float ExtractYawFromQuaternion(System.Numerics.Quaternion q)
|
||||
> {
|
||||
> // Standard z-up yaw extraction: atan2(2(wz + xy), 1 - 2(y² + z²))
|
||||
> return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y),
|
||||
> 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> ### Change 3: Rewrite `TickAnimations` env-var-on branch
|
||||
>
|
||||
> Find the existing env-var-on block in the per-frame remote tick (added at commit `ae79e34`). It currently looks roughly like:
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> if (rm.Interp.IsActive) {
|
||||
> float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
> Vector3 delta = rm.Interp.AdjustOffset((double)dt, rm.Body.Position, maxSpeed);
|
||||
> rm.Body.Position += delta;
|
||||
> }
|
||||
> rm.Body.UpdatePhysicsInternal(dt);
|
||||
> // entity write-back
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Replace with PositionManager call:
|
||||
>
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> // Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal
|
||||
> // (acclient @ 0x00513730):
|
||||
> // 1+2. animation root motion + interpolation correction (combined)
|
||||
> // 3. physics integration (gravity for airborne; no-op for grounded)
|
||||
> System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity
|
||||
> ?? System.Numerics.Vector3.Zero;
|
||||
> float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
> System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
|
||||
> dt: (double)dt,
|
||||
> currentBodyPosition: rm.Body.Position,
|
||||
> seqVel: seqVel,
|
||||
> ori: rm.Body.Orientation,
|
||||
> interp: rm.Interp,
|
||||
> maxSpeed: maxSpeed);
|
||||
> rm.Body.Position += offset;
|
||||
> rm.Body.UpdatePhysicsInternal(dt);
|
||||
> // KEEP whatever entity write-back lines were here (ae.Entity.Position = ..., etc.)
|
||||
> }
|
||||
> else
|
||||
> {
|
||||
> // EXISTING legacy path UNCHANGED
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> The `else` branch (legacy path) stays UNCHANGED.
|
||||
>
|
||||
> **Build + test:**
|
||||
>
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
> dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
> ```
|
||||
>
|
||||
> Expected: 0 build errors. Same 4 pre-existing failures (`DispatcherToMovementIntegrationTests` + `BSPStepUpTests` — these are not related to L.3 work). No NEW failures.
|
||||
>
|
||||
> **Commit:**
|
||||
>
|
||||
> ```bash
|
||||
> git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2)
|
||||
>
|
||||
> Combines PositionManager (Task 1) + IsGrounded plumbing (Task 2) into
|
||||
> the per-frame remote motion path. Three changes in GameWindow.cs,
|
||||
> all gated behind ACDREAM_INTERP_MANAGER=1:
|
||||
>
|
||||
> 1. RemoteMotion gains Position field (PositionManager instance).
|
||||
>
|
||||
> 2. OnLivePositionUpdated env-var branch rewritten to mirror retail
|
||||
> CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
|
||||
> - orientation snap-on-receipt (PositionManager handles position only)
|
||||
> - airborne (!IsGrounded) → no-op (server is authoritative for arc;
|
||||
> body.Velocity from VectorUpdate integrates gravity locally)
|
||||
> - landing transition (first IsGrounded=true after Airborne) →
|
||||
> clear airborne flags, hard-snap to landing pos, clear queue
|
||||
> - grounded routing: dist > 96m → slide-snap; dist ≤ 96m → enqueue
|
||||
>
|
||||
> 3. TickAnimations env-var branch rewritten to use PositionManager:
|
||||
> body.Position += PositionManager.ComputeOffset(dt, pos, seqVel,
|
||||
> ori, interp, maxSpeed); body.UpdatePhysicsInternal(dt) for gravity.
|
||||
>
|
||||
> Replaces the L.3.1-only AdjustOffset-only path. Legacy (env-var off)
|
||||
> path unchanged.
|
||||
>
|
||||
> Cleanup commit (next sub-task) deletes the env-var dual paths after
|
||||
> visual verification.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Self-review checklist:**
|
||||
> - [ ] `RemoteMotion.Position` field added (alongside existing `Interp`)
|
||||
> - [ ] `OnLivePositionUpdated` env-var branch has 3 sub-branches: airborne return, landing transition, grounded routing (snap or enqueue)
|
||||
> - [ ] `OnLivePositionUpdated` legacy `else` branch UNCHANGED
|
||||
> - [ ] `TickAnimations` env-var branch uses `PositionManager.ComputeOffset` exclusively (no direct `AdjustOffset` call)
|
||||
> - [ ] `TickAnimations` legacy `else` branch UNCHANGED
|
||||
> - [ ] `ExtractYawFromQuaternion` helper present (re-add if missing)
|
||||
> - [ ] `OnLiveVectorUpdated` UNTOUCHED (it already does the right thing)
|
||||
> - [ ] Build clean, same 4 pre-existing failures
|
||||
>
|
||||
> **Report:**
|
||||
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
> - Lines changed (with file:line refs)
|
||||
> - Test count
|
||||
> - Concerns (if any)
|
||||
>
|
||||
> If the existing legacy `else` path is so tangled that you can't safely rewrite the env-var branch without disturbing it, REPORT BLOCKED with specifics.
|
||||
|
||||
**Steps for the parent:**
|
||||
|
||||
- [ ] **Step 3.1: Dispatch the implementer subagent** using the prompt above.
|
||||
- [ ] **Step 3.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.App/Rendering/GameWindow.cs
|
||||
```
|
||||
- [ ] **Step 3.3: Build + test in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: 0 build errors. Same 4 pre-existing failures.
|
||||
- [ ] **Step 3.4: Spec compliance review** (general-purpose subagent). Verify the rewrite matches the spec's pseudocode exactly. Verify legacy `else` paths are byte-for-byte unchanged.
|
||||
- [ ] **Step 3.5: Code quality review** (`superpowers:code-reviewer`). Specifically check: orientation snap is in ALL routing paths; airborne no-op is the FIRST gate; landing transition resets all the right flags; ExtractYawFromQuaternion is correct.
|
||||
- [ ] **Step 3.6: Address review issues if any.** Fix subagent + re-review.
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — USER GATE: visual verification
|
||||
|
||||
**Owner:** User. Cannot be automated.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 4.1: Kill any running acdream**
|
||||
```powershell
|
||||
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 8
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Launch acdream with `ACDREAM_INTERP_MANAGER=1`**
|
||||
```powershell
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = "1"
|
||||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||
$env:ACDREAM_TEST_PORT = "9000"
|
||||
$env:ACDREAM_TEST_USER = "testaccount"
|
||||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||
$env:ACDREAM_INTERP_MANAGER = "1"
|
||||
dotnet run --project C:\Users\erikn\source\repos\acdream\src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "C:\Users\erikn\source\repos\acdream\.claude\worktrees\jovial-blackburn-773942\launch.log"
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Visual test matrix** with parallel retail observer of `+Acdream`. On the retail side, walk + run + jump + turn the toon and verify:
|
||||
|
||||
| Scenario | Expected |
|
||||
|---|---|
|
||||
| Walk forward 5 sec | acdream observer sees smooth glide, NO 1-Hz popping |
|
||||
| Walk backward 5 sec | smooth glide backward (regression check vs commit `17a9ff1`) |
|
||||
| Strafe left/right 5 sec each | smooth glide sideways |
|
||||
| Stop, then run forward 5 sec | smooth glide at run speed |
|
||||
| Jump from standstill 2-3× | curved arc, lands cleanly, NO endless rise |
|
||||
| Jump while running 2-3× | arc preserves forward motion, lands cleanly |
|
||||
| Turn quickly while running | heading tracks smoothly (not stuck at login direction) |
|
||||
|
||||
- [ ] **Step 4.4: User signs off OR files a regression**
|
||||
- If smooth + jumps land + turning works → proceed to Tasks 5+6.
|
||||
- If anything regresses → describe the symptom; parent dispatches a fix subagent or unsets the env-var for instant rollback.
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — Cleanup commit (parallel with Task 6)
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Task 6.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths + soft-snap fields)
|
||||
|
||||
**Subagent dispatch prompt:**
|
||||
|
||||
> You are implementing Task 5 of Phase L.3.1+L.3.2: cleanup. The user has visually verified that `ACDREAM_INTERP_MANAGER=1` works correctly. Now collapse the dual-path scaffolding.
|
||||
>
|
||||
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main — direct-to-main per CLAUDE.md.
|
||||
>
|
||||
> **What to do in `src/AcDream.App/Rendering/GameWindow.cs`:**
|
||||
>
|
||||
> 1. **In `OnLivePositionUpdated`**: delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { ... return; }` wrapper. Keep ONLY the new logic inside it. Delete the legacy hard-snap path that came after.
|
||||
>
|
||||
> 2. **In `TickAnimations` (per-frame remote tick)**: delete the `if/else` env-var gate. Keep ONLY the new path (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). Delete the legacy `apply_current_movement` + `force-OnWalkable` + Euler-extrapolate code in the `else` branch.
|
||||
>
|
||||
> 3. **In the `RemoteMotion` class** (~line 224): delete `SnapResidualDecayRate` and any soft-snap residual fields. Search for `_snapResidual`, `SnapResidualDecayRate`, `SoftSnap`. Also delete any related code in the call sites.
|
||||
>
|
||||
> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain:
|
||||
> ```bash
|
||||
> grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
|
||||
> ```
|
||||
> Expected: no output.
|
||||
>
|
||||
> **Build + test:**
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
> dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
> ```
|
||||
> Expected: 0 build errors. Same 4 pre-existing failures, no new ones.
|
||||
>
|
||||
> **Commit:**
|
||||
> ```bash
|
||||
> git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead legacy paths (L.3.1+L.3.2 cleanup)
|
||||
>
|
||||
> User has visually verified the new PositionManager + IsGrounded
|
||||
> routing path works correctly. Collapses the env-var dual-path:
|
||||
> deletes legacy hard-snap + apply_current_movement + Euler-extrapolate
|
||||
> code from OnLivePositionUpdated and the per-frame remote tick.
|
||||
> Deletes SnapResidualDecayRate + soft-snap residual fields from
|
||||
> RemoteMotion.
|
||||
>
|
||||
> Single retail-faithful path remains. ~80 lines net deletion.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Report:**
|
||||
> - Status, line counts deleted, files touched, test results.
|
||||
|
||||
**Steps for the parent:**
|
||||
|
||||
- [ ] **Step 5.1: Dispatch the cleanup subagent in parallel with Task 6** (one message, two Agent tool calls).
|
||||
- [ ] **Step 5.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat
|
||||
```
|
||||
- [ ] **Step 5.3: Confirm zero env-var references remain**
|
||||
```bash
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
|
||||
```
|
||||
Expected: no output.
|
||||
- [ ] **Step 5.4: Re-run all tests in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: same baseline.
|
||||
|
||||
---
|
||||
|
||||
## Task 6 — Roadmap + spec status update (parallel with Task 5)
|
||||
|
||||
**Owner:** Parent. Mechanical doc updates.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-04-11-roadmap.md` (Phase L.3 entry — mark L.3.1+L.3.2 SHIPPED)
|
||||
- Modify: `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` (add SHIPPED status banner)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 6.1: Find the Phase L.3 entry in the roadmap**
|
||||
```bash
|
||||
grep -n "Phase L.3\|L.3.1\|L.3.2\|L.3.3" "C:/Users/erikn/source/repos/acdream/docs/plans/2026-04-11-roadmap.md"
|
||||
```
|
||||
If the roadmap doesn't yet have a Phase L.3 entry, add one between L.2 and M with the L.3.1+L.3.2 combined status = SHIPPED, L.3.3 status = PLANNED.
|
||||
|
||||
- [ ] **Step 6.2: Update the spec doc's status**
|
||||
|
||||
In `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`, near the top (after the title / methodology), add or update a status line:
|
||||
|
||||
```markdown
|
||||
**Status:** L.3.1+L.3.2 SHIPPED 2026-05-02. L.3.3 PLANNED.
|
||||
```
|
||||
|
||||
- [ ] **Step 6.3: Commit (combined doc update)**
|
||||
```bash
|
||||
git add docs/plans/2026-04-11-roadmap.md docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs(roadmap+spec): Phase L.3.1+L.3.2 shipped (L.3.3 pending)
|
||||
|
||||
Roadmap Phase L.3 entry updated. Spec status banner reflects the
|
||||
combined L.3.1+L.3.2 deliverable as shipped after visual verification.
|
||||
L.3.3 (MoveToManager) remains a separate sub-lane to be specced and
|
||||
scheduled.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
End-to-end smoke test after Task 6:
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream
|
||||
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo # green
|
||||
dotnet test --no-build --nologo # 4 pre-existing failures only
|
||||
git log --oneline -10 # see commits in order
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed)
|
||||
grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted)
|
||||
```
|
||||
|
||||
User can re-run the visual test matrix WITHOUT setting `ACDREAM_INTERP_MANAGER` (default behavior is now the new path) and confirm parity.
|
||||
|
||||
If everything's green → Phase L.3.1+L.3.2 done; brainstorm L.3.3 (MoveToManager) as the next sub-lane.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** every section of the spec maps to a task here. PositionManager → Task 1; IsGrounded plumbing → Task 2; per-frame tick rewrite + RemoteMotion field + OnLivePositionUpdated rewrite → Task 3; cleanup → Task 5; doc updates → Task 6.
|
||||
- **Already-shipped commits NOT rebuilt.** L.3.1's first 6 commits (f43f168 → e08accf) already provide InterpolationManager + GetMaxSpeed + Interp field + v1 routing + v1 tick + Omega.
|
||||
- **Reverted commits** (5154a3e + f199a6a) were band-aids; their replacements live in Task 3.
|
||||
- **Subagent failure handling:** if a subagent reports BLOCKED on Task 3 (the largest), break it into smaller pieces (3a: RemoteMotion field; 3b: OnLivePositionUpdated rewrite; 3c: TickAnimations rewrite) and dispatch sequentially. Don't let a confused subagent leave broken code in main.
|
||||
- **Task 4's visual verification is the gate.** Tasks 5+6 only fire after user sign-off. If visual fails, dispatch a fix subagent before Tasks 5+6.
|
||||
|
|
@ -1,573 +0,0 @@
|
|||
# Phase L.3 — Remote Entity Motion Conformance — Design Spec
|
||||
|
||||
Port retail's `InterpolationManager` + `MoveOrTeleport` routing into
|
||||
acdream so remote players, creatures, and NPCs stop popping at every
|
||||
server position update and instead glide smoothly between sparse
|
||||
authoritative updates the way retail does.
|
||||
|
||||
**Methodology:** the named retail decomp at `docs/research/named-retail/`
|
||||
is ground truth. Live cdb traces against retail acclient.exe v11.4186
|
||||
have already resolved the open questions about constants and routing
|
||||
polarity (see *Research baseline* below). ACE and holtburger are
|
||||
secondary.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Remote-entity motion in acdream is choppy. Compared with retail
|
||||
observers, our remote-rendered players, creatures, and NPCs:
|
||||
|
||||
- **Pop visibly on every UpdatePosition** (~1 Hz for players, ~5 Hz
|
||||
for moving creatures). Each inbound 0xF748 hard-snaps `Body.Position`
|
||||
via `OnLivePositionUpdated` (`GameWindow.cs:3151`), then the local
|
||||
client extrapolates forward via `apply_current_movement` + Euler
|
||||
integration until the next server update arrives. Direction error
|
||||
compounds during the gap because acdream's locally-computed velocity
|
||||
may diverge from the server's authoritative state.
|
||||
- **Apply VectorUpdate.Omega = nothing.** `0xF74E` is parsed but the
|
||||
body's omega field is never written, so jumping/turning observers
|
||||
show flat arcs instead of curved ones.
|
||||
- **Run two parallel motion systems that fight each other.**
|
||||
`RemoteMotion` (in `GameWindow.cs:224`) carries
|
||||
`SnapResidualDecayRate` + a soft-snap residual blend that's an
|
||||
acdream-original heuristic. Retail does none of this; it has a
|
||||
single deterministic pipeline.
|
||||
- **Server-controlled creature MoveTo is MVP.** `RemoteMoveToDriver`
|
||||
uses a fixed turn rate, no retracking, no sticky-to-target, no
|
||||
fail-distance progress — chasing creatures and patrol-walking NPCs
|
||||
look approximate.
|
||||
|
||||
Result: the world feels jittery; remote characters teleport-then-glide
|
||||
in place of moving smoothly; jumps look wrong from observers.
|
||||
|
||||
---
|
||||
|
||||
## Research baseline
|
||||
|
||||
Resolved 2026-05-02 via cdb live-trace + named-decomp dive:
|
||||
|
||||
| Source | Path |
|
||||
|---|---|
|
||||
| **Resolution doc (canonical answers)** | `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` |
|
||||
| Original three agent reports (in worktrees) | `adoring-torvalds-d796cf`, `sleepy-grothendieck-9d7483`, `gracious-wright-7af984` |
|
||||
| cdb scripts + logs | `interp_discovery.cdb/log`, `interp_constants.cdb/log`, `interp_const2.cdb/log`, `interp_trace.cdb/log` (in worktree) |
|
||||
|
||||
**Key facts established by that work:**
|
||||
|
||||
- Retail does NOT velocity-dead-reckon walking remotes. `m_velocityVector`
|
||||
stays at zero; only `set_local_velocity` (called from `LeaveGround` =
|
||||
outbound jump) and `DoVectorUpdate` (inbound 0xF74E) ever touch it.
|
||||
- All visible motion comes from `InterpolationManager::adjust_offset`
|
||||
walking the body toward the head node of a FIFO position-waypoint
|
||||
queue at `2 × motion_max_speed × dt`.
|
||||
- `CPhysicsObj::MoveOrTeleport` is the routing decision: stale-seq →
|
||||
ignore; teleport-seq newer or no-cell → `SetPosition` hard-snap;
|
||||
has_contact && distance ≤ 96 → `InterpolateTo` (queue);
|
||||
has_contact && distance > 96 → `SetPositionSimple` slide-snap.
|
||||
|
||||
**Constants** (all confirmed by reading the binary's named constant
|
||||
addresses — not guesses):
|
||||
|
||||
| Constant | Value | Use |
|
||||
|---|---:|---|
|
||||
| `MAX_PHYSICS_DISTANCE` | 96 m | MoveOrTeleport router gate |
|
||||
| `CREATURE_OUTSIDE_BLIP_DISTANCE` | 100 m | InterpolateTo enqueue gate (outdoor) |
|
||||
| `CREATURE_INSIDE_BLIP_DISTANCE` | 20 m | InterpolateTo enqueue gate (indoor) |
|
||||
| `MAX_INTERPOLATED_VELOCITY_MOD` | 2.0 | `adjust_offset` catch-up gain × motion max |
|
||||
| `MAX_INTERPOLATED_VELOCITY` | 7.5 m/s | `adjust_offset` fallback when minterp unavailable |
|
||||
| `MIN_DISTANCE_TO_REACH_POSITION` | 0.20 m | per-5-frame stall progress threshold |
|
||||
| `DESIRED_DISTANCE` | 0.05 m | reach + duplicate-prune |
|
||||
| `max_velocity` | 50 m/s | `set_velocity` magnitude clamp |
|
||||
| Queue cap | 20 | `InterpolateTo` |
|
||||
| Stall window | 5 frames | `adjust_offset` periodic check |
|
||||
| Stall fail trigger | 3 fails / 30 % progress | `UseTime` blip-to-tail |
|
||||
|
||||
---
|
||||
|
||||
## Phase identity
|
||||
|
||||
**Phase L.3 — Remote Entity Motion Conformance.** Slots into the L =
|
||||
movement category alongside L.1 (animation) and L.2 (collision).
|
||||
|
||||
**Scope revision 2026-05-02 (after Task 7 visual verification):**
|
||||
L.3.1 was originally scoped as "InterpolationManager only", with L.3.2
|
||||
("PositionManager") deferred. Visual verification proved L.3.1 alone
|
||||
**cannot produce smooth motion** — retail combines animation root motion
|
||||
+ InterpolationManager corrections, and only the second half ships in
|
||||
L.3.1-as-originally-scoped. The two halves are functionally inseparable.
|
||||
|
||||
**L.3.1 and L.3.2 are now combined into a single sub-lane** ("L.3.1+L.3.2
|
||||
combined"). L.3.3 remains a separate sub-lane.
|
||||
|
||||
| Sub-lane | Title | Ships |
|
||||
|---|---|---|
|
||||
| **L.3.1 + L.3.2 combined** | InterpolationManager + PositionManager + retail-faithful jump | (1) `InterpolationManager` (FIFO queue + AdjustOffset), (2) `MotionInterpreter.GetMaxSpeed`, (3) `PositionManager` class combining animation root motion + Interp correction per frame, (4) `IsGrounded` plumbed through `EntityPositionUpdate`, (5) `OnLivePositionUpdated` retail-faithful routing (airborne no-op + landing transition + grounded routing), (6) per-frame `TickAnimations` calls `PositionManager.ComputeOffset` + `UpdatePhysicsInternal`, (7) `VectorUpdate.Omega` application |
|
||||
| **L.3.3** | MoveToManager (server-controlled creature MoveTo) | Replaces `RemoteMoveToDriver` MVP with a faithful port: retracking, sticky-to-target, fail-distance progress checks, sphere-cylinder distance variants |
|
||||
|
||||
L.3.3 gets its own brainstorm + spec when L.3.1+L.3.2 ships.
|
||||
**This document specifies L.3.1+L.3.2 in detail; L.3.3 is a sketch**
|
||||
(above) so the phase shape is on record.
|
||||
|
||||
### What changed since original spec
|
||||
|
||||
- **L.3.2 PositionManager** is now part of L.3.1, not a separate phase.
|
||||
- **`IsGrounded` plumbing** added — verified to already exist as
|
||||
`PositionFlags.IsGrounded = 0x04` in `UpdatePosition.cs:48`, parsed but
|
||||
not exposed through `EntityPositionUpdate`. Now exposed.
|
||||
- **Jump pipeline** rewritten to match retail's `MoveOrTeleport`
|
||||
has_contact=false → no-op semantics. Local arc prediction (the source
|
||||
of the "endless jump" bug) eliminated. Server is authoritative; landing
|
||||
detected via the first `IsGrounded=true` UP after airborne.
|
||||
- **Stall-blip → TAIL** (resolved Task 0 via decomp dive of
|
||||
`acclient!InterpolationManager::UseTime` @ 0x00555F20).
|
||||
- **Reverted band-aid commits** `5154a3e` + `f199a6a` (commit `1641d6e`)
|
||||
before re-implementing properly.
|
||||
|
||||
---
|
||||
|
||||
## L.3.1 architecture
|
||||
|
||||
### New file — `src/AcDream.Core/Physics/InterpolationManager.cs`
|
||||
|
||||
Pure-data class. No game/window dependencies. Composed into
|
||||
`RemoteMotion` (one instance per remote entity).
|
||||
|
||||
```csharp
|
||||
public sealed class InterpolationManager
|
||||
{
|
||||
// Public API
|
||||
void Enqueue(Position target, float ownerHeading, bool isMovingTo);
|
||||
Vector3 AdjustOffset(double dt, float maxSpeedFromMinterp); // returns body-space delta to add this frame
|
||||
bool IsActive { get; } // queue non-empty
|
||||
void Clear(); // StopInterpolating equivalent
|
||||
|
||||
// Internals
|
||||
private readonly LinkedList<InterpolationNode> _queue; // cap 20
|
||||
private int _failFrameCounter;
|
||||
private float _failDistanceLastCheck;
|
||||
private int _failCount;
|
||||
|
||||
// Constants (all from retail named symbols)
|
||||
public const int QueueCap = 20;
|
||||
public const float MaxInterpolatedVelocityMod = 2.0f;
|
||||
public const float MaxInterpolatedVelocity = 7.5f;
|
||||
public const float MinDistanceToReachPosition = 0.20f;
|
||||
public const float DesiredDistance = 0.05f;
|
||||
public const int StallCheckFrameInterval = 5;
|
||||
public const float StallProgressMinFraction = 0.30f;
|
||||
public const int StallFailCountForBlip = 3;
|
||||
}
|
||||
|
||||
internal sealed class InterpolationNode
|
||||
{
|
||||
public Position Target;
|
||||
public float Heading;
|
||||
public bool IsHeadingValid;
|
||||
}
|
||||
```
|
||||
|
||||
`AdjustOffset` algorithm (mirrors `acclient!InterpolationManager::adjust_offset`):
|
||||
|
||||
```text
|
||||
1. If queue empty → return Vector3.Zero
|
||||
2. headTarget = queue.First
|
||||
3. dist = (headTarget.Position - currentBodyPosition).Magnitude
|
||||
4. If dist < DesiredDistance:
|
||||
queue.RemoveFirst(); return Vector3.Zero (NodeCompleted)
|
||||
5. catchUpSpeed = clamp(maxSpeedFromMinterp * MaxInterpolatedVelocityMod,
|
||||
floor=F_EPSILON,
|
||||
else MaxInterpolatedVelocity fallback)
|
||||
6. step = catchUpSpeed * dt (clamped to dist so we don't overshoot)
|
||||
7. delta = (headTarget.Position - currentBodyPosition).Normalized * step
|
||||
8. _failFrameCounter++; if (_failFrameCounter >= StallCheckFrameInterval):
|
||||
progress = _failDistanceLastCheck - dist
|
||||
if (progress < StallProgressMinFraction * (catchUpSpeed * dt * StallCheckFrameInterval)):
|
||||
_failCount++
|
||||
if _failCount > StallFailCountForBlip:
|
||||
// blip: snap to TAIL (most recent server-sent waypoint) and clear queue.
|
||||
// RESOLVED 2026-05-02 via decomp dive of acclient!InterpolationManager::
|
||||
// UseTime @ 0x00555F20: lines 353273-353333 read this->position_queue.tail_,
|
||||
// copy tail.Position into local var, call CPhysicsObj::SetPositionSimple
|
||||
// on it, then StopInterpolating. Semantic: "warp to where the server
|
||||
// LAST SAID you are", not "where you were trying to get to next."
|
||||
tail = queue.Last
|
||||
body.Position = tail.TargetPosition // SetPositionSimple equivalent
|
||||
Clear()
|
||||
return Vector3.Zero
|
||||
else:
|
||||
_failCount = 0
|
||||
_failDistanceLastCheck = dist; _failFrameCounter = 0
|
||||
9. return delta
|
||||
```
|
||||
|
||||
`Enqueue` algorithm (mirrors `acclient!CPhysicsObj::InterpolateTo`):
|
||||
|
||||
```text
|
||||
1. If queue tail exists and Position.distance(target, tail.Target) < DesiredDistance:
|
||||
// Duplicate-prune
|
||||
return
|
||||
2. If queue.Count >= QueueCap: queue.RemoveLast() (drop oldest)
|
||||
3. node = new InterpolationNode { Target=target, Heading=ownerHeading, IsHeadingValid=isMovingTo }
|
||||
4. queue.AddLast(node)
|
||||
```
|
||||
|
||||
### Modified — `RemoteMotion` (in `GameWindow.cs:224`)
|
||||
|
||||
Add: `public InterpolationManager Interp { get; } = new();`
|
||||
|
||||
Delete (in cleanup commit, after visual verification):
|
||||
`SnapResidualDecayRate` constant + soft-snap residual fields
|
||||
(`_snapResidual*`, etc).
|
||||
|
||||
### Modified — `OnLivePositionUpdated` (`GameWindow.cs:3151`)
|
||||
|
||||
Replace the unconditional hard-snap with retail-faithful routing.
|
||||
Wrap in `ACDREAM_INTERP_MANAGER=1` env-var gate so we can toggle old
|
||||
vs new during development.
|
||||
|
||||
```csharp
|
||||
void OnLivePositionUpdated(EntityPositionUpdate update)
|
||||
{
|
||||
var rm = GetOrCreateRemoteMotion(update.Guid);
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
// Retail-faithful routing (CPhysicsObj::MoveOrTeleport).
|
||||
// IsStaleSequence: wrap-aware uint16 compare on the four sequence
|
||||
// counters (instance, position, teleport, force-position) the
|
||||
// server stamps on every UpdatePosition. Wrap window = 0x7FFF.
|
||||
// See acclient!CPhysicsObj::newer_event @ 0x00451B10.
|
||||
if (IsStaleSequence(update, rm)) return;
|
||||
|
||||
if (update.TeleportSequenceNewer || rm.Body.NoCell)
|
||||
{
|
||||
rm.Body.Position = targetPosition; // SetPosition hard-snap
|
||||
rm.Interp.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!update.HasContact) return; // no-op
|
||||
|
||||
// Distance source: retail uses this->[+0x20] which is the entity's
|
||||
// distance to the local player. acdream computes the equivalent on
|
||||
// demand here — local player position is _playerController.Position.
|
||||
float dist = Vector3.Distance(targetPosition, _playerController.Position);
|
||||
if (dist > 96f) {
|
||||
rm.Interp.Clear(); // StopInterpolating
|
||||
rm.Body.Position = targetPosition; // SetPositionSimple slide-snap
|
||||
} else {
|
||||
// headingFromQuat: extract Z-axis heading from the wire quaternion.
|
||||
// Use existing acdream Quat→Yaw helper (mirrors GameWindow's
|
||||
// YawToAcQuaternion in reverse). isMovingTo gates whether the heading
|
||||
// is preserved across InterpolateTo's "same target" path.
|
||||
rm.Interp.Enqueue(targetPosition, headingFromQuat, isMovingTo: rm.IsMovingTo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Existing hard-snap path (unchanged) — kept until cleanup commit
|
||||
rm.Body.Position = targetPosition;
|
||||
rm.SnapResidualDecayRate = ...;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modified — per-frame remote tick (`OnLiveRemoteTick` in `GameWindow`)
|
||||
|
||||
When flag on: ask the InterpolationManager for its catch-up offset and
|
||||
add it to the body's position. When flag off: existing
|
||||
apply_current_movement + Euler path.
|
||||
|
||||
```csharp
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
float maxSpeed = rm.Motion.GetMaxSpeed(); // see MotionInterpreter change below
|
||||
Vector3 delta = rm.Interp.AdjustOffset(dt, maxSpeed);
|
||||
rm.Body.Position += delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Existing apply_current_movement + Euler (unchanged) — kept until cleanup commit
|
||||
}
|
||||
```
|
||||
|
||||
### Modified — `OnLiveVectorUpdated` (`GameWindow.cs:3064`)
|
||||
|
||||
Apply `update.Omega` to the body. Currently parsed-but-ignored. ~3 lines:
|
||||
|
||||
```csharp
|
||||
if (update.Velocity is { } v) rm.Body.Velocity = v;
|
||||
if (update.Omega is { } w) rm.Body.AngularVel = w; // NEW
|
||||
```
|
||||
|
||||
### Modified — `MotionInterpreter`
|
||||
|
||||
Add `public float GetMaxSpeed()` — port of retail
|
||||
`CMotionInterp::get_max_speed` and `get_adjusted_max_speed`. Returns
|
||||
the motion-table-derived max speed for the current InterpretedState.
|
||||
Used by InterpolationManager via the caller (RemoteMotion's tick).
|
||||
|
||||
Public method, ~10 lines, no new file.
|
||||
|
||||
### Cleanup commit (after visual verification)
|
||||
|
||||
One commit titled `chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead soft-snap path`:
|
||||
|
||||
- Delete the `if/else` env-var gate in `OnLivePositionUpdated` and
|
||||
`TickAnimations` per-frame remote tick. Keep only the new path.
|
||||
- Delete `RemoteMotion.SnapResidualDecayRate` field + soft-snap
|
||||
residual fields.
|
||||
- Delete the apply_current_movement + Euler dead-reckoning code in
|
||||
the per-frame remote tick (the OLD branch).
|
||||
|
||||
Net diff after cleanup: ~80 lines deletion, code shrinks.
|
||||
|
||||
---
|
||||
|
||||
## L.3.2 architecture (PositionManager — combined into L.3.1)
|
||||
|
||||
### New file — `src/AcDream.Core/Physics/PositionManager.cs`
|
||||
|
||||
Pure-data class, no game/window deps. Pure function: takes (animation
|
||||
root motion + body orientation + InterpolationManager + maxSpeed) and
|
||||
returns the per-frame world-space delta to add to `body.Position`.
|
||||
Composed into `RemoteMotion` alongside the `Interp` field.
|
||||
|
||||
**API:**
|
||||
|
||||
```csharp
|
||||
public sealed class PositionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-frame combiner: animation root motion + InterpolationManager
|
||||
/// correction. Mirrors retail CPhysicsObj::UpdateObjectInternal
|
||||
/// (acclient @ 0x00513730):
|
||||
/// rootOffset = CPartArray::Update(dt) // animation
|
||||
/// PositionManager::adjust_offset(rootOffset) // adds correction
|
||||
/// frame.origin += rootOffset
|
||||
/// </summary>
|
||||
public Vector3 ComputeOffset(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
Vector3 seqVel, // body-local velocity from active animation cycle
|
||||
Quaternion ori, // body orientation (for local→world rotation)
|
||||
InterpolationManager interp,
|
||||
float maxSpeed)
|
||||
{
|
||||
// Step 1: animation root motion (body-local → world).
|
||||
Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
|
||||
|
||||
// Step 2: interpolation correction (world-space already).
|
||||
Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
|
||||
|
||||
// Step 3: combined.
|
||||
return rootMotionWorld + correction;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composition
|
||||
|
||||
`RemoteMotion` (in `GameWindow.cs:224`) gains a second field:
|
||||
|
||||
```csharp
|
||||
public AcDream.Core.Physics.PositionManager Position { get; } =
|
||||
new AcDream.Core.Physics.PositionManager();
|
||||
```
|
||||
|
||||
(Already has `public InterpolationManager Interp` from Task 3.)
|
||||
|
||||
### Per-frame `TickAnimations` (env-var-on branch)
|
||||
|
||||
```csharp
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
// Always-run-all-steps per retail UpdateObjectInternal (0x00513730).
|
||||
Vector3 seqVel = ae.Sequencer?.CurrentVelocity ?? Vector3.Zero;
|
||||
float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
Vector3 offset = rm.Position.ComputeOffset(
|
||||
dt, rm.Body.Position, seqVel, rm.Body.Orientation, rm.Interp, maxSpeed);
|
||||
rm.Body.Position += offset;
|
||||
rm.Body.UpdatePhysicsInternal(dt); // gravity for airborne; no-op for grounded
|
||||
}
|
||||
else { /* legacy path (kept until cleanup commit) */ }
|
||||
```
|
||||
|
||||
Replaces the Task 5 commit's `if (rm.Interp.IsActive) { ... AdjustOffset ... }`
|
||||
block. PositionManager calls AdjustOffset internally.
|
||||
|
||||
### IsGrounded plumbing — `EntityPositionUpdate`
|
||||
|
||||
`PositionFlags.IsGrounded = 0x04` is already parsed in
|
||||
`UpdatePosition.cs:48`. Add a `bool IsGrounded` field to
|
||||
`EntityPositionUpdate` record, populate at the parse site, consume in
|
||||
`OnLivePositionUpdated`. ~3 lines.
|
||||
|
||||
### Retail-faithful jump pipeline
|
||||
|
||||
Rewrites the `OnLivePositionUpdated` env-var-on branch to match retail
|
||||
`MoveOrTeleport` (acclient @ 0x00516330) — `has_contact=false → return`:
|
||||
|
||||
```csharp
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
rmState.Body.Orientation = rot; // orientation always snaps
|
||||
|
||||
if (!update.IsGrounded) // airborne: no-op
|
||||
return;
|
||||
|
||||
if (rmState.Airborne) // landing transition
|
||||
{
|
||||
rmState.Airborne = false;
|
||||
rmState.Body.Velocity = Vector3.Zero;
|
||||
rmState.Body.State &= ~PhysicsStateFlags.Gravity;
|
||||
rmState.Body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos; // hard-snap to landing
|
||||
return;
|
||||
}
|
||||
|
||||
// Grounded routing (CPhysicsObj::MoveOrTeleport):
|
||||
const float MaxPhysicsDistance = 96f;
|
||||
var localPlayerPos = _playerController?.Position ?? Vector3.Zero;
|
||||
float dist = Vector3.Distance(worldPos, localPlayerPos);
|
||||
|
||||
if (dist > MaxPhysicsDistance)
|
||||
{
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||
rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
`OnLiveVectorUpdated` is **unchanged** — already sets velocity, marks
|
||||
airborne, enables Gravity, applies Omega (Task 6).
|
||||
|
||||
### L.3.2 unit tests
|
||||
|
||||
New test file `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs`,
|
||||
~6 tests against the pure `ComputeOffset` function:
|
||||
|
||||
| Test | Verifies |
|
||||
|---|---|
|
||||
| `ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion` | seqVel=0, queue empty → returns Vector3.Zero |
|
||||
| `ComputeOffset_AnimationOnly_Forward_BodyAdvances` | seqVel=(0,4,0), identity orientation → returns (0, 4*dt, 0) |
|
||||
| `ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth` | seqVel=(0,4,0), orientation faces -Y → returns (0,-4*dt,0) |
|
||||
| `ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue` | seqVel=0, queue active → returns Interp's delta |
|
||||
| `ComputeOffset_BothActive_Combined` | both nonzero → returns sum |
|
||||
| `ComputeOffset_LocalToWorldRotation_Yaw90` | seqVel=(0,1,0), yaw=π/2 → returns (sin(π/2), cos(π/2)·1, 0) verifying rotation |
|
||||
|
||||
---
|
||||
|
||||
## L.3.1 unit tests
|
||||
|
||||
New test file `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs`. ~10-15 tests covering pure-data behavior:
|
||||
|
||||
| Group | Tests |
|
||||
|---|---|
|
||||
| Queue mechanics | `Enqueue_AddsNode`; `Enqueue_DropsOldestAtCap20`; `Enqueue_PrunesDuplicateWithinDesiredDistance`; `Clear_EmptiesQueue` |
|
||||
| AdjustOffset math | `AdjustOffset_EmptyQueue_ReturnsZero`; `AdjustOffset_ReachesNodeWithinDesiredDistance_PopsHead`; `AdjustOffset_ClampedToCatchUpSpeed`; `AdjustOffset_FallbackSpeedWhenMinterpZero`; `AdjustOffset_OvershootProtection` |
|
||||
| Stall detection | `AdjustOffset_StallCounterFiresEvery5Frames`; `AdjustOffset_NoProgressMarksFail`; `AdjustOffset_3FailsTriggersBlipToHead`; `AdjustOffset_GoodProgressResetsFailCount` |
|
||||
| Routing helpers | (in `MoveOrTeleportRoutingTests.cs` — separate file) `Routing_StaleSequence_Skips`; `Routing_TeleportSeqNewer_HardSnaps`; `Routing_NoContact_NoOp`; `Routing_Within96_Enqueues`; `Routing_Beyond96_SlideSnaps` |
|
||||
|
||||
All tests run against a stub `Body` and stub motion-max-speed value —
|
||||
no game/window/loader needed.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
L.3.1+L.3.2 (combined) is shippable when:
|
||||
|
||||
1. `dotnet build` green; existing 105 unit tests + 16 InterpolationManager + 5 GetMaxSpeed + ~6 PositionManager tests all pass.
|
||||
2. **Visual primary:** parallel retail observer of `+Acdream` standing still, walking, running, strafing, turning — **all motion glides smoothly**, no 1-Hz popping. (PositionManager's animation-root-motion is what eliminates the chop.)
|
||||
3. **Visual jump:** retail toon jumping shows a curved arc that LANDS correctly (no endless rise). Server-authoritative airborne (`IsGrounded=false → no-op`).
|
||||
4. **Visual regression check:** behaviors fixed in commit `17a9ff1` (backward jump direction, strafe-run animation, walk-back broadcast direction) all still work.
|
||||
5. After visual confirmation: cleanup commit lands removing `ACDREAM_INTERP_MANAGER` flag + old hard-snap path + dead `RemoteMotion` soft-snap fields.
|
||||
|
||||
---
|
||||
|
||||
## Risks + mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| New routing interacts badly with `OnLiveVectorUpdated` (jump path runs in parallel) | env-var flag lets us A/B in seconds; visual jump test in acceptance |
|
||||
| `MotionInterpreter.GetMaxSpeed()` returns wrong value for non-locomotion states | add to unit tests; fall back to `MAX_INTERPOLATED_VELOCITY = 7.5` if returns ≤ epsilon |
|
||||
| `_cameraPosition` for the 96 m gate is the local player's pos (retail uses `this->[+0x20]` = entity-to-local-player distance) — same thing in our setup | document the assumption inline; revisit in L.3.2 if PositionManager wants a different definition |
|
||||
| `ACDREAM_INTERP_MANAGER=1` flag forgotten in cleanup commit | acceptance criterion #5 makes the cleanup commit a gate item |
|
||||
| Queue grows unbounded if NodeCompleted check is buggy | cap-at-20 in Enqueue is hard limit; unit test exercises drop-oldest |
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
### Already shipped (L.3.1 original scope)
|
||||
|
||||
- `src/AcDream.Core/Physics/InterpolationManager.cs` (commits `f43f168` + `927636e`)
|
||||
- `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs` (16 tests)
|
||||
- `src/AcDream.Core/Physics/MotionInterpreter.cs` `GetMaxSpeed()` (commits `9c5634a` + `5b26d28`)
|
||||
- `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` (5 GetMaxSpeed tests added)
|
||||
- `RemoteMotion.Interp` field (commit `517a3ce`)
|
||||
- `OnLivePositionUpdated` env-var routing v1 (commit `062e19f`)
|
||||
- Per-frame `Interp.AdjustOffset` v1 (commit `ae79e34`)
|
||||
- `OnLiveVectorUpdated.Omega` application (commit `e08accf`)
|
||||
- Reverted band-aid commits (commit `1641d6e`)
|
||||
|
||||
### To ship (L.3.2 added scope)
|
||||
|
||||
**New:**
|
||||
- `src/AcDream.Core/Physics/PositionManager.cs` — pure-data combiner class
|
||||
- `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` — ~6 tests
|
||||
|
||||
**Modified:**
|
||||
- `src/AcDream.Core.Net/WorldSession.cs` — add `IsGrounded` field to `EntityPositionUpdate` record + populate at parse site (~3 lines)
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs`:
|
||||
- `RemoteMotion` (~line 224): add `Position` field (alongside existing `Interp`)
|
||||
- `OnLivePositionUpdated` env-var branch: rewritten — airborne no-op + landing transition + grounded routing (replaces the existing v1 routing)
|
||||
- `TickAnimations` env-var branch: rewritten — `PositionManager.ComputeOffset` + `UpdatePhysicsInternal` (replaces the existing v1 Interp.AdjustOffset call)
|
||||
|
||||
### Cleanup commit (after verification)
|
||||
|
||||
Single commit: collapses env-var dual paths to retail-faithful path,
|
||||
deletes `RemoteMotion` soft-snap residual fields. ~80 lines deletion.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (deferred to L.3.3)
|
||||
|
||||
- Server-controlled MoveTo creature behavior (retracking, sticky, fail-distance) — L.3.3
|
||||
- Replacing `RemoteMoveToDriver.cs` — L.3.3
|
||||
- VectorUpdate.Omega for other entity types (projectiles, dropped items) — defer; current spec applies only to player/creature/NPC paths
|
||||
|
||||
---
|
||||
|
||||
## Implementation order (L.3.1+L.3.2 combined — remaining work)
|
||||
|
||||
Original L.3.1 commits 1-6 already shipped. The two band-aid commits (`5154a3e`, `f199a6a`) reverted in `1641d6e`. Remaining:
|
||||
|
||||
1. **`feat(physics): PositionManager class + 6 unit tests`** — subagent-implemented. Pure-data class + tests against stub Interp.
|
||||
2. **`feat(net): plumb IsGrounded through EntityPositionUpdate`** — parent edit, 3 lines.
|
||||
3. **`feat(motion): retail-faithful per-frame remote tick (PositionManager + IsGrounded routing)`** — subagent. Adds `RemoteMotion.Position` field + rewrites both env-var-on branches (`OnLivePositionUpdated` and the per-frame tick). Single commit because changes are tightly coupled.
|
||||
4. **USER GATE — visual verification** with retail observer of `+Acdream` performing the test matrix.
|
||||
5. **`chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead legacy paths`** — subagent. Cleanup commit.
|
||||
6. **`docs(roadmap+spec): L.3.1+L.3.2 combined; L.3.3 still separate`** — parent. Roadmap entry update + spec status mark.
|
||||
|
||||
Each step is one commit. Direct-to-main per CLAUDE.md.
|
||||
|
||||
---
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Research: `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md`
|
||||
- Findings (prior session): `docs/research/2026-05-01-retail-motion-trace/findings.md`
|
||||
- Memory: `memory/project_retail_motion_outbound.md`, `memory/project_retail_debugger.md`
|
||||
- Roadmap: insert as Phase L.3 in `docs/plans/2026-04-11-roadmap.md`
|
||||
- Related fixed issues: `17a9ff1 fix(motion)` (backward/strafe wire + jump direction) — L.3.1 verifies this still works
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -64,7 +64,6 @@ public static class UpdatePosition
|
|||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
uint? PlacementId,
|
||||
bool IsGrounded,
|
||||
ushort InstanceSequence = 0,
|
||||
ushort TeleportSequence = 0,
|
||||
ushort ForcePositionSequence = 0);
|
||||
|
|
@ -165,7 +164,6 @@ public static class UpdatePosition
|
|||
RotationW: rw, RotationX: rx, RotationY: ry, RotationZ: rz);
|
||||
|
||||
return new Parsed(guid, serverPos, velocity, placementId,
|
||||
IsGrounded: (flags & PositionFlags.IsGrounded) != 0,
|
||||
instSeq, teleSeq, forceSeq);
|
||||
}
|
||||
catch
|
||||
|
|
|
|||
|
|
@ -110,8 +110,7 @@ public sealed class WorldSession : IDisposable
|
|||
public readonly record struct EntityPositionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
bool IsGrounded);
|
||||
System.Numerics.Vector3? Velocity);
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the session parses a 0xF748 UpdatePosition game message.
|
||||
|
|
@ -712,8 +711,7 @@ public sealed class WorldSession : IDisposable
|
|||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||
posUpdate.Value.Guid,
|
||||
posUpdate.Value.Position,
|
||||
posUpdate.Value.Velocity,
|
||||
posUpdate.Value.IsGrounded));
|
||||
posUpdate.Value.Velocity));
|
||||
}
|
||||
}
|
||||
else if (op == VectorUpdate.Opcode)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@
|
|||
<PackageReference Include="Chorizite.DatReaderWriter" Version="2.1.7" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>AcDream.Core.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -282,14 +282,6 @@ public sealed class AnimationSequencer
|
|||
private const double FrameEpsilon = 1e-5;
|
||||
private const double RateEpsilon = 1e-6;
|
||||
|
||||
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
||||
// Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines
|
||||
// emitted from SetCycle. Gated on env var ACDREAM_REMOTE_VEL_DIAG=1; reads
|
||||
// the env var inline rather than caching so a launch can be re-toggled
|
||||
// without restarting. 0.5s per sequencer instance keeps logs readable
|
||||
// while still capturing meaningful state changes.
|
||||
private double _lastSetCycleDiagTime;
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -414,23 +406,6 @@ public sealed class AnimationSequencer
|
|||
MultiplyCyclicFramerate(speedMod / CurrentSpeedMod);
|
||||
CurrentSpeedMod = speedMod;
|
||||
}
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFAST — proves whether the fast-path
|
||||
// is firing instead of the full rebuild. Throttled to 0.5s per
|
||||
// instance (re-throttled after A.1 unthrottled experiment).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFAST] motion=0x{motion:X8} speedMod={speedMod:F3} "
|
||||
+ $"oldSpeedMod={CurrentSpeedMod:F3} "
|
||||
+ $"qCount={_queue.Count} "
|
||||
+ $"currNodeIsCyclic={(_currNode == _firstCyclic)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -472,21 +447,6 @@ public sealed class AnimationSequencer
|
|||
// add_motion chain (MotionTable.cs L100-L101, L152-L153).
|
||||
ClearPhysics();
|
||||
|
||||
// Snapshot the queue tail BEFORE appending new motion data so we
|
||||
// can locate the first newly-added node afterward and force
|
||||
// _currNode onto it. Without this, _currNode can stay pointing
|
||||
// into stale non-cyclic head frames left over from the previous
|
||||
// cycle (typically a Walk_link or Ready_link's tail), and the
|
||||
// visible animation continues playing those stale frames before
|
||||
// the queue advances naturally to the new cycle. For remote
|
||||
// entities receiving many bundled UMs over time, this stale-head
|
||||
// build-up was the root cause of "transitions between cycles
|
||||
// don't visibly switch the leg pose" even though SetCycle's
|
||||
// CurrentMotion/CurrentSpeedMod were updated correctly. Local
|
||||
// player avoided the bug because PlayerMovementController fires
|
||||
// SetCycle in a tight per-input loop that keeps the queue clean.
|
||||
var preEnqueueTail = _queue.Last;
|
||||
|
||||
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||
if (linkData is { Anims.Count: > 0 })
|
||||
EnqueueMotionData(linkData, adjustedSpeed, isLooping: false);
|
||||
|
|
@ -518,57 +478,11 @@ public sealed class AnimationSequencer
|
|||
}
|
||||
}
|
||||
|
||||
// Force _currNode onto the FIRST NEWLY-ENQUEUED node so the
|
||||
// visible animation switches to the new cycle/link immediately
|
||||
// instead of finishing whatever stale head frames were sitting
|
||||
// at the front of the queue. preEnqueueTail.Next is the first
|
||||
// newly-added node; if preEnqueueTail was null (queue was empty
|
||||
// before enqueue), the first new node is _queue.First.
|
||||
var firstNew = preEnqueueTail is null ? _queue.First : preEnqueueTail.Next;
|
||||
if (firstNew is not null)
|
||||
// If we have no current anim, start at the beginning of the queue.
|
||||
if (_currNode == null)
|
||||
{
|
||||
_currNode = firstNew;
|
||||
_framePosition = _currNode.Value.GetStartFramePosition();
|
||||
}
|
||||
else if (_currNode == null)
|
||||
{
|
||||
// Defensive fallback: nothing newly added AND no current node.
|
||||
_currNode = _queue.First;
|
||||
_framePosition = _currNode?.Value.GetStartFramePosition() ?? 0.0;
|
||||
|
||||
// D4 (Commit A 2026-05-03): SCNULLFALLBACK — proves whether the
|
||||
// null-data fallback is being hit. If this fires during a
|
||||
// Walk→Run transition for the watched remote, H4 (MotionTable
|
||||
// GetLink/GetCycle returns null for the remote's setup) is the
|
||||
// bug. linkData/cycleData null almost certainly means a
|
||||
// MotionTable lookup gap for that style+motion combo.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCNULLFALLBACK] motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||
+ $"linkNull={(linkData is null)} cycleNull={(cycleData is null)} "
|
||||
+ $"qCount={_queue.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFULL — counterpart to SCFAST. Fires on
|
||||
// the full-rebuild SetCycle path. Throttled to 0.5s per instance.
|
||||
// Logs prev CurrentMotion so the line shows the transition directly
|
||||
// (e.g. "Run → Ready" = cycle just got reset).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFULL] prev=0x{CurrentMotion:X8} -> motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||
+ $"speedMod={speedMod:F3} "
|
||||
+ $"qCount={_queue.Count} "
|
||||
+ $"firstNewNull={(firstNew is null)} "
|
||||
+ $"currNodeIsCyclic={(_currNode == _firstCyclic)} "
|
||||
+ $"firstCyclicNull={(_firstCyclic is null)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentStyle = style;
|
||||
|
|
@ -596,52 +510,42 @@ public sealed class AnimationSequencer
|
|||
// decompiled from _DAT_007c96e0/e4/e8. The velocity is body-local
|
||||
// (+Y = forward, +X = right); consumers rotate into world space via
|
||||
// the owning entity's orientation.
|
||||
// For known locomotion cycles, ALWAYS overwrite CurrentVelocity with
|
||||
// the synthesized value — even if the transition link set
|
||||
// CurrentVelocity from its own HasVelocity flag. The link's velocity
|
||||
// is for the brief transition (e.g. small stride into run-pose); the
|
||||
// cycle's intended steady-state velocity is what consumers (remote
|
||||
// body translation in GameWindow.TickAnimations env-var path) need.
|
||||
// Without this, walking-to-running transitions left CurrentVelocity
|
||||
// at the link's slow pace, and the user reported "it just blips
|
||||
// forward walking" until another motion command (turn, etc) forced
|
||||
// a re-synth. The gate that previously read
|
||||
// `if (CurrentVelocity.LengthSquared() < 1e-9f)` allowed dat-baked
|
||||
// velocity to win over synthesis — which is correct for non-
|
||||
// locomotion (e.g. flying creatures with HasVelocity) but wrong for
|
||||
// Humanoid run/walk/strafe where the dat is silent and the link
|
||||
// velocity is the only thing setting it.
|
||||
if (CurrentVelocity.LengthSquared() < 1e-9f)
|
||||
{
|
||||
float yvel = 0f;
|
||||
float xvel = 0f;
|
||||
// Low byte of the ORIGINAL (non-adjusted) motion tells us which
|
||||
// intent the caller signalled. adjust_motion may have remapped
|
||||
// TurnLeft → TurnRight / SideStepLeft → SideStepRight /
|
||||
// WalkBackward → WalkForward, encoding the sign into adjustedSpeed.
|
||||
// The speed sign is preserved in adjustedSpeed so we multiply by
|
||||
// it rather than re-deriving per-case.
|
||||
uint low = motion & 0xFFu;
|
||||
bool isLocomotion = false;
|
||||
switch (low)
|
||||
{
|
||||
case 0x05: // WalkForward
|
||||
yvel = WalkAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x06: // WalkBackward — adjust_motion remapped to WalkForward
|
||||
// with speedMod *= -0.65f.
|
||||
// with speedMod *= -0.65f, so adjustedSpeed already
|
||||
// carries the factor. But the motion arg we see
|
||||
// here is the original (pre-adjust) 0x06, so we
|
||||
// still use WalkAnimSpeed — the negative sign of
|
||||
// adjustedSpeed flips the direction correctly.
|
||||
yvel = WalkAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x07: // RunForward
|
||||
yvel = RunAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x0F: // SideStepRight
|
||||
xvel = SidestepAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x10: // SideStepLeft — remapped to SideStepRight with
|
||||
// negated speed; same handling as backward walk.
|
||||
xvel = SidestepAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
}
|
||||
if (isLocomotion)
|
||||
if (yvel != 0f || xvel != 0f)
|
||||
CurrentVelocity = new Vector3(xvel, yvel, 0f);
|
||||
}
|
||||
|
||||
|
|
@ -663,15 +567,10 @@ public sealed class AnimationSequencer
|
|||
case 0x0D: // TurnRight — clockwise from above = -Z in right-handed.
|
||||
zomega = -(MathF.PI / 2f) * adjustedSpeed;
|
||||
break;
|
||||
case 0x0E: // TurnLeft — counter-clockwise = +Z.
|
||||
// adjust_motion above ALREADY remapped 0x0E → 0x0D
|
||||
// with adjustedSpeed = -speedMod, so the same
|
||||
// formula as 0x0D applied to the negated speed
|
||||
// produces the correct +Z (CCW) result. Using a
|
||||
// different sign here would double-negate and
|
||||
// animate a left turn as a right turn — that was
|
||||
// the bug observed before this fix (commit follows).
|
||||
zomega = -(MathF.PI / 2f) * adjustedSpeed;
|
||||
case 0x0E: // TurnLeft — counter-clockwise = +Z. adjust_motion
|
||||
// may have remapped 0x0E → 0x0D with negated speed;
|
||||
// in that case the negation preserves correct sign.
|
||||
zomega = (MathF.PI / 2f) * adjustedSpeed;
|
||||
break;
|
||||
}
|
||||
if (zomega != 0f)
|
||||
|
|
|
|||
|
|
@ -1,329 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// InterpolationManager — retail CPhysicsObj interpolation queue.
|
||||
//
|
||||
// Ports:
|
||||
// CPhysicsObj::InterpolateTo (acclient @ 0x005104F0)
|
||||
// InterpolationManager::adjust_offset (acclient @ 0x00555D30)
|
||||
// InterpolationManager::UseTime (acclient @ 0x00555F20) — stall/blip
|
||||
//
|
||||
// FIFO position-waypoint queue (cap 20). On each physics tick the caller
|
||||
// passes current body position + max-speed from the motion table; we return
|
||||
// the delta vector to apply to the body for this frame.
|
||||
//
|
||||
// Queue semantics:
|
||||
// - Head = next target. Body walks toward head at catch-up speed.
|
||||
// - Tail = most-recent server position. On stall we blip directly to tail
|
||||
// (retail UseTime @ 0x00555F20: copies tail_ position, calls
|
||||
// CPhysicsObj::SetPositionSimple, then StopInterpolating).
|
||||
//
|
||||
// Constants verified from named binary at the addresses cited above (not
|
||||
// guesses):
|
||||
// MAX_INTERPOLATED_VELOCITY_MOD = 2.0
|
||||
// MAX_INTERPOLATED_VELOCITY = 7.5
|
||||
// MIN_DISTANCE_TO_REACH_POSITION = 0.20 (absolute stall threshold, meters)
|
||||
// DESIRED_DISTANCE = 0.05
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Waypoint used internally by <see cref="InterpolationManager"/>.
|
||||
/// </summary>
|
||||
internal sealed class InterpolationNode
|
||||
{
|
||||
public Vector3 TargetPosition;
|
||||
public float Heading;
|
||||
public bool IsHeadingValid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-remote-entity position interpolation queue. Caller enqueues server
|
||||
/// position updates and calls <see cref="AdjustOffset"/> once per physics
|
||||
/// tick to get the per-frame correction delta.
|
||||
/// </summary>
|
||||
public sealed class InterpolationManager
|
||||
{
|
||||
// ── public constants (retail binary values) ───────────────────────────────
|
||||
|
||||
/// <summary>Maximum waypoints held before oldest is dropped.</summary>
|
||||
public const int QueueCap = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Catch-up gain: catchUpSpeed = motionMaxSpeed × this modifier.
|
||||
/// Retail MAX_INTERPOLATED_VELOCITY_MOD (@ 0x00555D30).
|
||||
/// </summary>
|
||||
public const float MaxInterpolatedVelocityMod = 2.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Fallback catch-up speed (m/s) when motion-table max speed is
|
||||
/// unavailable (zero/tiny).
|
||||
/// Retail MAX_INTERPOLATED_VELOCITY (@ 0x00555D30).
|
||||
/// </summary>
|
||||
public const float MaxInterpolatedVelocity = 7.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Per-5-frame stall progress threshold (meters). Body must advance at
|
||||
/// least this far in <see cref="StallCheckFrameInterval"/> frames or
|
||||
/// the window counts as a stall.
|
||||
/// Retail MIN_DISTANCE_TO_REACH_POSITION (@ 0x00555E42).
|
||||
/// </summary>
|
||||
public const float MinDistanceToReachPosition = 0.20f;
|
||||
|
||||
/// <summary>
|
||||
/// Reach + duplicate-prune radius (meters). Node is popped when
|
||||
/// distance to its target falls below this value; new enqueues within
|
||||
/// this distance of the tail are ignored.
|
||||
/// Retail DESIRED_DISTANCE (@ 0x00555D30).
|
||||
/// </summary>
|
||||
public const float DesiredDistance = 0.05f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of ticks between stall progress checks.
|
||||
/// Retail frame_counter threshold (@ 0x00555E14).
|
||||
/// </summary>
|
||||
public const int StallCheckFrameInterval = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum fraction of cumulative progress_quantum that counts as "real
|
||||
/// progress" in a stall check window. Below this fraction the window
|
||||
/// counts as a stall (secondary check, applies when progress_quantum > 0).
|
||||
/// Retail CREATURE_FAILED_INTERPOLATION_PERCENTAGE (@ 0x00555E73).
|
||||
/// </summary>
|
||||
public const float StallProgressMinFraction = 0.30f;
|
||||
|
||||
/// <summary>
|
||||
/// Stall-fail counter threshold. The body is blipped to the tail of the
|
||||
/// queue when <c>node_fail_counter</c> EXCEEDS this value (i.e., on the
|
||||
/// 4th consecutive failed window, not the 3rd).
|
||||
/// Retail: <c>node_fail_counter > 3</c> (@ 0x00555F39).
|
||||
/// </summary>
|
||||
public const int StallFailCountThreshold = 3;
|
||||
|
||||
// ── internals ─────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly LinkedList<InterpolationNode> _queue = new();
|
||||
|
||||
/// <summary>Frames elapsed since the last 5-frame stall-check window fired.</summary>
|
||||
private int _framesSinceLastStallCheck = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative sum of per-frame <c>step</c> magnitudes within the current
|
||||
/// 5-frame window. Retail <c>progress_quantum</c>.
|
||||
/// </summary>
|
||||
private float _progressQuantum = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Distance to the head node recorded at the START of the current
|
||||
/// 5-frame window. Retail <c>original_distance</c>.
|
||||
/// </summary>
|
||||
private float _distanceAtWindowStart = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// True once the first valid distance sample has been taken and
|
||||
/// <c>_distanceAtWindowStart</c> is populated. Guards against the
|
||||
/// first-window false-positive that occurs when the field defaults to 0.
|
||||
/// </summary>
|
||||
private bool _haveBaselineDistance = false;
|
||||
|
||||
/// <summary>
|
||||
/// Number of consecutive 5-frame windows that failed both the absolute
|
||||
/// and ratio progress checks. Retail <c>node_fail_counter</c>.
|
||||
/// Blip fires when this EXCEEDS <see cref="StallFailCountThreshold"/>.
|
||||
/// </summary>
|
||||
private int _failCount = 0;
|
||||
|
||||
// ── public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the queue holds at least one waypoint.</summary>
|
||||
public bool IsActive => _queue.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current waypoint count (visible to the test assembly for cap verification).
|
||||
/// </summary>
|
||||
internal int Count => _queue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Stop interpolating: clear the queue and reset all stall counters.
|
||||
/// Retail StopInterpolating / destructor cleanup.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_queue.Clear();
|
||||
_framesSinceLastStallCheck = 0;
|
||||
_progressQuantum = 0f;
|
||||
_distanceAtWindowStart = 0f;
|
||||
_haveBaselineDistance = false;
|
||||
_failCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue a new server-authoritative position waypoint.
|
||||
///
|
||||
/// <para>
|
||||
/// Step 1: Duplicate-prune — if the new target is within
|
||||
/// <see cref="DesiredDistance"/> of the current tail, ignore it.<br/>
|
||||
/// Step 2: Cap — if the queue is already at <see cref="QueueCap"/>,
|
||||
/// drop the oldest (head) entry.<br/>
|
||||
/// Step 3/4: Append a new <see cref="InterpolationNode"/>.
|
||||
/// </para>
|
||||
///
|
||||
/// Retail CPhysicsObj::InterpolateTo (@ 0x005104F0).
|
||||
/// </summary>
|
||||
/// <param name="targetPosition">Server-reported world position.</param>
|
||||
/// <param name="heading">Server-reported heading (radians, AC convention).</param>
|
||||
/// <param name="isMovingTo">True when the body is in motion — gates heading validity.</param>
|
||||
public void Enqueue(Vector3 targetPosition, float heading, bool isMovingTo)
|
||||
{
|
||||
// Step 1: duplicate-prune
|
||||
if (_queue.Last is { } last)
|
||||
{
|
||||
if (Vector3.Distance(targetPosition, last.Value.TargetPosition) < DesiredDistance)
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: enforce cap
|
||||
if (_queue.Count >= QueueCap)
|
||||
_queue.RemoveFirst();
|
||||
|
||||
// Steps 3+4: add node
|
||||
var node = new InterpolationNode
|
||||
{
|
||||
TargetPosition = targetPosition,
|
||||
Heading = heading,
|
||||
IsHeadingValid = isMovingTo,
|
||||
};
|
||||
_queue.AddLast(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the per-frame position correction delta.
|
||||
///
|
||||
/// <para>
|
||||
/// Returns <see cref="Vector3.Zero"/> when the queue is empty or when
|
||||
/// the head node has been reached. Returns a snap delta (tail −
|
||||
/// currentBodyPosition) after <see cref="StallFailCountThreshold"/>
|
||||
/// consecutive stall failures (i.e., fail count EXCEEDS the threshold),
|
||||
/// then clears the queue.
|
||||
/// </para>
|
||||
///
|
||||
/// Retail InterpolationManager::adjust_offset (@ 0x00555D30) +
|
||||
/// UseTime stall/blip (@ 0x00555F20).
|
||||
/// </summary>
|
||||
/// <param name="dt">Frame delta time (seconds).</param>
|
||||
/// <param name="currentBodyPosition">Current world-space body position.</param>
|
||||
/// <param name="maxSpeedFromMinterp">
|
||||
/// Max motion-table speed for this entity's current cycle (m/s), as
|
||||
/// reported by MotionInterpreter. Pass 0 if unavailable; the fallback
|
||||
/// <see cref="MaxInterpolatedVelocity"/> will be used.
|
||||
/// </param>
|
||||
/// <returns>World-space delta to apply to the body this frame.</returns>
|
||||
public Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)
|
||||
{
|
||||
// Guard: bad dt → skip entirely to prevent NaN poisoning PhysicsBody.Position.
|
||||
if (dt <= 0 || double.IsNaN(dt)) return Vector3.Zero;
|
||||
|
||||
// Step 1: empty queue → no correction
|
||||
if (_queue.First is null)
|
||||
return Vector3.Zero;
|
||||
|
||||
// Step 2: peek head
|
||||
var headNode = _queue.First.Value;
|
||||
|
||||
// Step 3: distance to head target
|
||||
float dist = (headNode.TargetPosition - currentBodyPosition).Length();
|
||||
|
||||
// Step 4: reached node
|
||||
if (dist < DesiredDistance)
|
||||
{
|
||||
_queue.RemoveFirst();
|
||||
return Vector3.Zero;
|
||||
}
|
||||
|
||||
// Step 5: compute catch-up speed
|
||||
float scaled = maxSpeedFromMinterp * MaxInterpolatedVelocityMod;
|
||||
float catchUpSpeed = scaled > 1e-6f ? scaled : MaxInterpolatedVelocity;
|
||||
|
||||
// Step 6: step magnitude (no overshoot)
|
||||
float step = catchUpSpeed * (float)dt;
|
||||
if (step > dist)
|
||||
step = dist;
|
||||
|
||||
// Step 7: direction × step
|
||||
Vector3 delta = ((headNode.TargetPosition - currentBodyPosition) / dist) * step;
|
||||
|
||||
// Step 8: stall detection (retail adjust_offset @ 0x00555E08-0x00555E92)
|
||||
//
|
||||
// Retail tracks two quantities across each 5-frame window:
|
||||
// progress_quantum — cumulative sum of per-frame step magnitudes
|
||||
// original_distance — distance to head at the START of the window
|
||||
//
|
||||
// At window end (frame_counter >= 5):
|
||||
// cumulative_progress = original_distance - currentDist
|
||||
//
|
||||
// Primary check (@ 0x00555E42):
|
||||
// cumulative_progress < MIN_DISTANCE_TO_REACH_POSITION (0.20 m)
|
||||
// → window is a stall; increment node_fail_counter.
|
||||
//
|
||||
// Secondary check (@ 0x00555E73, only when progress_quantum > 0):
|
||||
// cumulative_progress / progress_quantum < CREATURE_FAILED_INTERPOLATION_PERCENTAGE (0.30)
|
||||
// → window is a stall; increment node_fail_counter.
|
||||
//
|
||||
// Both checks operate with sticky_object_id == 0 (we never have one).
|
||||
// Either check failing counts the window as a stall.
|
||||
//
|
||||
// Blip fires when node_fail_counter > 3 (retail UseTime @ 0x00555F39).
|
||||
// Window always resets (frame_counter=0, progress_quantum=0,
|
||||
// original_distance=currentDist) after the check.
|
||||
|
||||
// Initialise window baseline on first call after Clear / new motion.
|
||||
if (!_haveBaselineDistance)
|
||||
{
|
||||
_distanceAtWindowStart = dist;
|
||||
_haveBaselineDistance = true;
|
||||
}
|
||||
|
||||
_progressQuantum += step;
|
||||
_framesSinceLastStallCheck++;
|
||||
|
||||
if (_framesSinceLastStallCheck >= StallCheckFrameInterval)
|
||||
{
|
||||
float cumulativeProgress = _distanceAtWindowStart - dist;
|
||||
|
||||
bool primaryFail = cumulativeProgress < MinDistanceToReachPosition;
|
||||
bool secondaryFail = _progressQuantum > 0f &&
|
||||
(cumulativeProgress / _progressQuantum) < StallProgressMinFraction;
|
||||
|
||||
if (primaryFail || secondaryFail)
|
||||
{
|
||||
_failCount++;
|
||||
// Blip-to-tail: retail UseTime (@ 0x00555F20) reads
|
||||
// position_queue.tail_, copies its position to a local,
|
||||
// calls CPhysicsObj::SetPositionSimple, then
|
||||
// StopInterpolating. Snap target is the TAIL (the most
|
||||
// recent server position), not the head.
|
||||
if (_failCount > StallFailCountThreshold)
|
||||
{
|
||||
Vector3 tailPos = _queue.Last!.Value.TargetPosition;
|
||||
Clear();
|
||||
return tailPos - currentBodyPosition;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_failCount = 0;
|
||||
}
|
||||
|
||||
// Reset the 5-frame window regardless of pass/fail.
|
||||
_framesSinceLastStallCheck = 0;
|
||||
_progressQuantum = 0f;
|
||||
_distanceAtWindowStart = dist;
|
||||
}
|
||||
|
||||
// Step 9: return per-frame delta
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
|
@ -932,58 +932,6 @@ public sealed class MotionInterpreter
|
|||
apply_current_movement(cancelMoveTo: false, allowJump: true);
|
||||
}
|
||||
|
||||
// ── CMotionInterp::get_max_speed (0x00527cb0) ─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Return the run rate. Mirrors retail
|
||||
/// <c>CMotionInterp::get_max_speed</c> at <c>0x00527cb0</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Decomp (named-retail/acclient_2013_pseudo_c.txt:305127):</b>
|
||||
/// <code>
|
||||
/// void get_max_speed(this) {
|
||||
/// weenie_obj = this->weenie_obj;
|
||||
/// this_1 = nullptr;
|
||||
/// if (weenie_obj == 0) return;
|
||||
/// if (weenie_obj->vtable->InqRunRate(&this_1) != 0) return;
|
||||
/// this->my_run_rate; // x87 fld leaves my_run_rate on FPU stack
|
||||
/// }
|
||||
/// </code>
|
||||
/// Binary Ninja shows the return type as <c>void</c> because the float
|
||||
/// return rides the x87 FPU stack rather than EAX. Both branches
|
||||
/// emit an <c>fld</c> of either <c>this_1</c> (the InqRunRate
|
||||
/// out-param value) or <c>my_run_rate</c>, leaving the run rate on
|
||||
/// ST0 as the return value.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Critical:</b> this returns the BARE run rate (typically 1.0 to
|
||||
/// ~3.0), NOT a velocity in m/s. We previously multiplied by
|
||||
/// <c>RunAnimSpeed</c> to get a m/s value, reasoning that
|
||||
/// <c>2 × bare_rate</c> would be too slow a catch-up speed for the
|
||||
/// caller (<c>InterpolationManager::adjust_offset</c>). That was a
|
||||
/// misread of the decomp — retail's catch-up IS that slow on purpose.
|
||||
/// The multi-second 1-Hz blip the user reported when observing retail
|
||||
/// remotes from acdream traced to body racing at the wrong (overshot)
|
||||
/// catch-up speed (~23.5 m/s instead of the retail-correct ~5.9 m/s
|
||||
/// for a run-skill-200 char).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public float GetMaxSpeed()
|
||||
{
|
||||
// Resolve current run rate: prefer WeenieObj.InqRunRate, fall back to MyRunRate.
|
||||
// Then multiply by RunAnimSpeed (4.0). Matches ACE's MotionInterp.cs:670-678
|
||||
// which is verified against retail (the ACE MotionInterp file is a
|
||||
// line-by-line port). Returns the maximum world-space velocity in m/s
|
||||
// — for run skill 200 with rate ≈ 2.94, this is ≈ 11.76 m/s. Used by
|
||||
// InterpolationManager.AdjustOffset to compute the catch-up speed
|
||||
// (= 2 × maxSpeed).
|
||||
float rate = MyRunRate;
|
||||
if (WeenieObj is not null && WeenieObj.InqRunRate(out float queried))
|
||||
rate = queried;
|
||||
return RunAnimSpeed * rate;
|
||||
}
|
||||
|
||||
// ── private helper ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame combiner for remote-entity motion: animation root motion
|
||||
/// + InterpolationManager catch-up correction. Pure function — no
|
||||
/// side effects, no hidden state.
|
||||
///
|
||||
/// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730):
|
||||
/// rootOffset = CPartArray::Update(dt) // animation
|
||||
/// PositionManager::adjust_offset(rootOffset) // adds correction
|
||||
/// frame.origin += rootOffset
|
||||
///
|
||||
/// In acdream the animation root motion is sourced from
|
||||
/// AnimationSequencer.CurrentVelocity (body-local velocity from the
|
||||
/// active locomotion cycle). We rotate that by the body's orientation
|
||||
/// to get a world-space delta, then add the InterpolationManager's
|
||||
/// world-space correction.
|
||||
/// </summary>
|
||||
public sealed class PositionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute the per-frame world-space delta to add to body.Position.
|
||||
/// </summary>
|
||||
/// <param name="dt">Per-frame delta time, seconds.</param>
|
||||
/// <param name="currentBodyPosition">Body's current world-space position.</param>
|
||||
/// <param name="seqVel">
|
||||
/// Body-local velocity from the active animation cycle
|
||||
/// (from <c>AnimationSequencer.CurrentVelocity</c>); pass
|
||||
/// <c>Vector3.Zero</c> if the entity has no sequencer or is on a
|
||||
/// non-locomotion cycle.
|
||||
/// </param>
|
||||
/// <param name="ori">Body orientation; used to rotate seqVel from body-local to world.</param>
|
||||
/// <param name="interp">The remote's InterpolationManager (for AdjustOffset call).</param>
|
||||
/// <param name="maxSpeed">From <c>MotionInterpreter.GetMaxSpeed()</c> — passed to AdjustOffset for the catch-up clamp.</param>
|
||||
public Vector3 ComputeOffset(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
Vector3 seqVel,
|
||||
Quaternion ori,
|
||||
InterpolationManager interp,
|
||||
float maxSpeed)
|
||||
{
|
||||
// Retail-faithful per-frame combiner. Mirrors
|
||||
// CPhysicsObj::UpdatePositionInternal (acclient @ 0x00512c30) +
|
||||
// InterpolationManager::adjust_offset (@ 0x00555d30):
|
||||
//
|
||||
// 1. CPartArray::Update writes rootOffset (animation root motion)
|
||||
// into the per-tick Frame.
|
||||
// 2. PositionManager::adjust_offset → InterpolationManager::adjust_offset
|
||||
// either:
|
||||
// a) RETURNS EARLY when distance(body, head) < 0.05m
|
||||
// (NodeCompleted; arg2 unmodified) — body uses root motion.
|
||||
// b) OVERWRITES arg2 with `direction × min(catchUpSpeed × dt,
|
||||
// distance)` when body is far from head — catch-up REPLACES
|
||||
// root motion for this frame.
|
||||
//
|
||||
// It is NOT additive. Our prior port added rootMotion + correction
|
||||
// every frame, which stacked the animation push (≈ RunAnimSpeed ×
|
||||
// speedMod, ≈ 11.7 m/s) on top of the queue catch-up (capped at
|
||||
// ≈ 23.5 m/s) so the body advanced at up to ~3× the server's
|
||||
// broadcast pace and the head-behind-body case produced a backward
|
||||
// correction every UP — the visible 1-Hz blip the user reported.
|
||||
//
|
||||
// AdjustOffset returns Vector3.Zero in two cases mapped to retail's
|
||||
// early-return: empty queue OR distance < DesiredDistance (0.05m).
|
||||
// In both, body falls back to animation root motion.
|
||||
Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
|
||||
if (correction.LengthSquared() > 0f)
|
||||
return correction;
|
||||
|
||||
Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
return Vector3.Transform(rootMotionLocal, ori);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,384 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// InterpolationManagerTests — covers the retail CPhysicsObj interpolation
|
||||
// queue port (L.3.1 Task 1).
|
||||
//
|
||||
// Source addresses tested:
|
||||
// CPhysicsObj::InterpolateTo acclient @ 0x005104F0 (Enqueue)
|
||||
// InterpolationManager::adjust_offset acclient @ 0x00555D30 (AdjustOffset)
|
||||
// InterpolationManager::UseTime acclient @ 0x00555F20 (blip-to-tail)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public sealed class InterpolationManagerTests
|
||||
{
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Origin used as the "body is here" position in most tests.</summary>
|
||||
private static readonly Vector3 BodyOrigin = Vector3.Zero;
|
||||
|
||||
/// <summary>A position clearly outside DesiredDistance (= 0.05 m).</summary>
|
||||
private static readonly Vector3 FarTarget = new Vector3(10f, 0f, 0f);
|
||||
|
||||
private static InterpolationManager Make() => new InterpolationManager();
|
||||
|
||||
// =========================================================================
|
||||
// Queue mechanics
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void Enqueue_AddsNode_QueueBecomesActive()
|
||||
{
|
||||
var mgr = Make();
|
||||
Assert.False(mgr.IsActive);
|
||||
|
||||
mgr.Enqueue(FarTarget, heading: 0f, isMovingTo: true);
|
||||
|
||||
Assert.True(mgr.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enqueue_DropsOldestWhenAtCap20()
|
||||
{
|
||||
var mgr = Make();
|
||||
|
||||
// Fill the queue to cap with distinct positions spaced far enough
|
||||
// apart to avoid the duplicate-prune threshold (DesiredDistance = 0.05).
|
||||
for (int i = 0; i < InterpolationManager.QueueCap; i++)
|
||||
{
|
||||
mgr.Enqueue(new Vector3(i * 1f, 0f, 0f), heading: 0f, isMovingTo: true);
|
||||
}
|
||||
|
||||
// Sanity: queue is at cap before the 21st enqueue.
|
||||
Assert.Equal(InterpolationManager.QueueCap, mgr.Count);
|
||||
|
||||
// The 21st enqueue must drop the oldest (x=0) and keep the count at cap.
|
||||
mgr.Enqueue(new Vector3(100f, 0f, 0f), heading: 0f, isMovingTo: true);
|
||||
|
||||
// Count must still be QueueCap — not QueueCap+1.
|
||||
Assert.Equal(InterpolationManager.QueueCap, mgr.Count);
|
||||
|
||||
// The head (oldest surviving node) must now be x=1 (the second-original
|
||||
// position), not x=0 (which was dropped). Verify by driving the body
|
||||
// to exactly x=1 — AdjustOffset must pop that node (distance < DesiredDistance)
|
||||
// and return zero, confirming x=1 is the head.
|
||||
var bodyAtSecondOriginal = new Vector3(1f, 0f, 0f);
|
||||
var result = mgr.AdjustOffset(
|
||||
dt: 0.016,
|
||||
currentBodyPosition: bodyAtSecondOriginal,
|
||||
maxSpeedFromMinterp: 10f);
|
||||
|
||||
// Reached head (dist ≈ 0) → zero delta + node popped.
|
||||
Assert.Equal(Vector3.Zero, result);
|
||||
// One node was consumed; count must now be QueueCap - 1.
|
||||
Assert.Equal(InterpolationManager.QueueCap - 1, mgr.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enqueue_AtCap20_HeadIsSecondOriginal()
|
||||
{
|
||||
// Complementary test for the cap overflow: after 21 enqueues the
|
||||
// second-enqueued position (x=1) must be at the head, not x=0.
|
||||
var mgr = Make();
|
||||
for (int i = 0; i < InterpolationManager.QueueCap; i++)
|
||||
{
|
||||
mgr.Enqueue(new Vector3(i * 1f, 0f, 0f), heading: 0f, isMovingTo: true);
|
||||
}
|
||||
mgr.Enqueue(new Vector3(100f, 0f, 0f), heading: 0f, isMovingTo: true);
|
||||
|
||||
// Place the body far away from x=0 but RIGHT on x=1. If x=0 were the
|
||||
// head the result would be non-zero (body is 1 m away from x=0).
|
||||
// If x=1 is the head the distance is 0 → pop → zero return.
|
||||
var bodyAtX1 = new Vector3(1f, 0f, 0f);
|
||||
var delta = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: bodyAtX1, maxSpeedFromMinterp: 10f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, delta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enqueue_PrunesDuplicateWithinDesiredDistance()
|
||||
{
|
||||
var mgr = Make();
|
||||
var basePos = new Vector3(5f, 0f, 0f);
|
||||
|
||||
mgr.Enqueue(basePos, heading: 0f, isMovingTo: true);
|
||||
|
||||
// Within DesiredDistance (0.05) — must be ignored.
|
||||
var nearDuplicate = basePos + new Vector3(0.01f, 0f, 0f);
|
||||
mgr.Enqueue(nearDuplicate, heading: 0f, isMovingTo: true);
|
||||
|
||||
// Confirm duplicate was not added: driving the body to basePos should
|
||||
// exhaust the queue in one pop, leaving it empty.
|
||||
// Position body exactly AT the target so AdjustOffset pops the head node.
|
||||
var result = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: basePos, maxSpeedFromMinterp: 10f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, result); // reached → pop
|
||||
Assert.False(mgr.IsActive); // only one node existed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_EmptiesQueueAndResetsCounters()
|
||||
{
|
||||
var mgr = Make();
|
||||
mgr.Enqueue(FarTarget, heading: 0f, isMovingTo: true);
|
||||
Assert.True(mgr.IsActive);
|
||||
|
||||
mgr.Clear();
|
||||
|
||||
Assert.False(mgr.IsActive);
|
||||
|
||||
// After Clear, AdjustOffset must return zero (no stale state).
|
||||
var delta = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
Assert.Equal(Vector3.Zero, delta);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AdjustOffset math
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_EmptyQueue_ReturnsZero()
|
||||
{
|
||||
var mgr = Make();
|
||||
var delta = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, delta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_ReachesNodeWithinDesiredDistance_PopsHead()
|
||||
{
|
||||
var mgr = Make();
|
||||
var target = new Vector3(0.02f, 0f, 0f); // within DesiredDistance (0.05)
|
||||
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
// Body is at origin; distance = 0.02 < 0.05 → should pop and return zero.
|
||||
var delta = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, delta);
|
||||
Assert.False(mgr.IsActive, "Head node should have been popped after being reached");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_ClampedToCatchUpSpeed_2xMotionMax()
|
||||
{
|
||||
var mgr = Make();
|
||||
float maxSpeed = 4.0f; // motion-table max speed
|
||||
double dt = 0.5; // large dt to make the math clear
|
||||
// target is far enough that there's no overshoot clamping
|
||||
var target = new Vector3(100f, 0f, 0f);
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
var delta = mgr.AdjustOffset(dt, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: maxSpeed);
|
||||
|
||||
// Expected step = catchUpSpeed * dt = (maxSpeed * 2.0) * dt = 4.0
|
||||
float expectedStep = maxSpeed * InterpolationManager.MaxInterpolatedVelocityMod * (float)dt;
|
||||
Assert.Equal(expectedStep, delta.Length(), precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_FallbackSpeed_WhenMinterpZero()
|
||||
{
|
||||
var mgr = Make();
|
||||
double dt = 0.5;
|
||||
var target = new Vector3(100f, 0f, 0f);
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
// maxSpeedFromMinterp = 0 → fallback to MaxInterpolatedVelocity (7.5)
|
||||
var delta = mgr.AdjustOffset(dt, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 0f);
|
||||
|
||||
float expectedStep = InterpolationManager.MaxInterpolatedVelocity * (float)dt;
|
||||
Assert.Equal(expectedStep, delta.Length(), precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_OvershootProtection_StepClampedToDistance()
|
||||
{
|
||||
var mgr = Make();
|
||||
float maxSpeed = 10f;
|
||||
double dt = 1.0; // step = 2*10*1.0 = 20 >> actual distance
|
||||
|
||||
// Place target just 0.5 m away — inside the step distance.
|
||||
var target = new Vector3(0.5f, 0f, 0f);
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
var delta = mgr.AdjustOffset(dt, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: maxSpeed);
|
||||
|
||||
// Step should be clamped to dist (0.5), not the unclamped 20.
|
||||
Assert.Equal(0.5f, delta.Length(), precision: 4);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Stall detection
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_StallCounterIncrementsEachFrame()
|
||||
{
|
||||
// Run 4 frames (< StallCheckFrameInterval = 5) with a body that does
|
||||
// not move — the queue should still be active (no blip yet).
|
||||
var mgr = Make();
|
||||
var target = new Vector3(10f, 0f, 0f);
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
// Body does NOT move — we pass the same fixed position each frame.
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
}
|
||||
|
||||
// After 4 frames (<5) the stall check hasn't fired yet, queue intact.
|
||||
Assert.True(mgr.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_NoProgressMarksFail_AfterFiveFrames()
|
||||
{
|
||||
// Body stays at origin every frame — zero real progress.
|
||||
// After 5 frames the stall check fires and _failCount increments (to 1).
|
||||
// Queue must still be alive (blip only at > StallFailCountThreshold = 3).
|
||||
var mgr = Make();
|
||||
var target = new Vector3(50f, 0f, 0f);
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
for (int i = 0; i < InterpolationManager.StallCheckFrameInterval; i++)
|
||||
{
|
||||
mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
}
|
||||
|
||||
// 1 fail < StallFailCountThreshold (3), so queue is still active.
|
||||
Assert.True(mgr.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_GoodProgressResetsFailCount()
|
||||
{
|
||||
// Simulate: body truly advances toward target each frame.
|
||||
// After each check-interval the fail counter should reset to 0
|
||||
// (because progress ≥ 30% of expected).
|
||||
var mgr = Make();
|
||||
var origin = Vector3.Zero;
|
||||
var target = new Vector3(50f, 0f, 0f);
|
||||
float maxSpd = 4f;
|
||||
double dt = 0.016;
|
||||
mgr.Enqueue(target, heading: 0f, isMovingTo: true);
|
||||
|
||||
// Run 5 frames, advancing the body by the actual delta returned each time.
|
||||
Vector3 bodyPos = origin;
|
||||
for (int i = 0; i < InterpolationManager.StallCheckFrameInterval; i++)
|
||||
{
|
||||
var delta = mgr.AdjustOffset(dt, currentBodyPosition: bodyPos, maxSpeedFromMinterp: maxSpd);
|
||||
bodyPos += delta; // body truly moves
|
||||
}
|
||||
|
||||
// After 5 frames of genuine progress, queue must still be active
|
||||
// (no blip) and _failCount should have been reset to 0 (no way to read
|
||||
// it directly, but we verify indirectly: we'd need 3×5=15 more frames
|
||||
// of stalling to blip — a further 5-frame no-progress window at this
|
||||
// point should only bring _failCount to 1, not trigger a blip).
|
||||
Assert.True(mgr.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_3FailsTriggersBlipToTail()
|
||||
{
|
||||
// Need > StallFailCountThreshold (3) failures.
|
||||
// Each failure requires one stall-check window (5 frames of no progress).
|
||||
// So we need 4 × 5 = 20 frames with the body frozen at origin.
|
||||
//
|
||||
// Also enqueue a SECOND node (the tail) different from the first, so we
|
||||
// can verify the snap is to the tail, not the head.
|
||||
var mgr = Make();
|
||||
var head = new Vector3(10f, 0f, 0f);
|
||||
var tail = new Vector3(30f, 0f, 0f);
|
||||
|
||||
mgr.Enqueue(head, heading: 0f, isMovingTo: true);
|
||||
mgr.Enqueue(tail, heading: 0f, isMovingTo: true);
|
||||
|
||||
// 4 stall-check windows × 5 frames each = 20 frames, body never moves.
|
||||
Vector3? blipDelta = null;
|
||||
const int totalFrames = (InterpolationManager.StallFailCountThreshold + 1)
|
||||
* InterpolationManager.StallCheckFrameInterval;
|
||||
|
||||
for (int i = 0; i < totalFrames; i++)
|
||||
{
|
||||
var delta = mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
if (delta.Length() > 1f) // blip delta will be >> normal per-frame step
|
||||
{
|
||||
blipDelta = delta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Blip must have fired.
|
||||
Assert.NotNull(blipDelta);
|
||||
|
||||
// Blip delta = tailPos − currentBodyPosition = (30,0,0) − (0,0,0)
|
||||
Assert.Equal(tail.X, blipDelta!.Value.X, precision: 4);
|
||||
Assert.Equal(tail.Y, blipDelta!.Value.Y, precision: 4);
|
||||
Assert.Equal(tail.Z, blipDelta!.Value.Z, precision: 4);
|
||||
|
||||
// Queue must be cleared after blip (retail StopInterpolating).
|
||||
Assert.False(mgr.IsActive);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// New tests: I-1 first-window false-positive guard, I-3 dt guard, I-5 cap
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_FirstWindow_DoesNotFalseFail()
|
||||
{
|
||||
// Before the fix, _distanceAtWindowStart defaulted to 0, so on the
|
||||
// first 5-frame window cumulative_progress = 0 - dist = -dist < 0.20
|
||||
// → every new motion sequence triggered a spurious stall fail.
|
||||
//
|
||||
// After the fix, the baseline is seeded from the first call, so
|
||||
// cumulative_progress = dist(frame0) - dist(frame4) which for a body
|
||||
// that hasn't moved yet is ≈ 0. That is still < MIN (0.20), but the
|
||||
// _failCount starts at 0 and must be > 3 (not == 1) to blip. The key
|
||||
// assertion is that after exactly ONE stall window the queue is still
|
||||
// alive (fail count == 1, blip requires > 3).
|
||||
var mgr = Make();
|
||||
mgr.Enqueue(new Vector3(50f, 0f, 0f), heading: 0f, isMovingTo: true);
|
||||
|
||||
// Run exactly one check-window (5 frames) with the body frozen.
|
||||
for (int i = 0; i < InterpolationManager.StallCheckFrameInterval; i++)
|
||||
{
|
||||
mgr.AdjustOffset(dt: 0.016, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
}
|
||||
|
||||
// One window fail → _failCount == 1, far below StallFailCountThreshold (3).
|
||||
// Queue must still be active; no spurious blip on first window.
|
||||
Assert.True(mgr.IsActive,
|
||||
"First stall window must NOT trigger a blip (would require > 3 consecutive failures).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_DtZeroOrNegative_ReturnsZero()
|
||||
{
|
||||
var mgr = Make();
|
||||
mgr.Enqueue(FarTarget, heading: 0f, isMovingTo: true);
|
||||
|
||||
// dt == 0 → guard fires, return zero, no side-effects.
|
||||
var deltaZero = mgr.AdjustOffset(dt: 0.0, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
Assert.Equal(Vector3.Zero, deltaZero);
|
||||
|
||||
// dt < 0 → guard fires, return zero.
|
||||
var deltaNeg = mgr.AdjustOffset(dt: -1.0, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
Assert.Equal(Vector3.Zero, deltaNeg);
|
||||
|
||||
// dt = NaN → guard fires, return zero.
|
||||
var deltaNaN = mgr.AdjustOffset(dt: double.NaN, currentBodyPosition: BodyOrigin, maxSpeedFromMinterp: 4f);
|
||||
Assert.Equal(Vector3.Zero, deltaNaN);
|
||||
|
||||
// Queue must still be intact (guards did not consume or corrupt state).
|
||||
Assert.True(mgr.IsActive);
|
||||
}
|
||||
}
|
||||
|
|
@ -817,75 +817,4 @@ public sealed class MotionInterpreterTests
|
|||
var vel = mi.get_state_velocity();
|
||||
Assert.Equal(4.0f * 2.375f, vel.Y, precision: 2);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GetMaxSpeed (CMotionInterp::get_max_speed @ 0x00527cb0)
|
||||
// L.3.1 Task 2 — InterpolationManager catch-up speed source
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRate()
|
||||
{
|
||||
// Retail: get_max_speed returns run rate from InqRunRate; callers
|
||||
// multiply by 2 to get catch-up speed. For RunForward the per-m/s
|
||||
// speed is RunAnimSpeed × rate = 4.0 × 1.5 = 6.0.
|
||||
var weenie = new FakeWeenie { RunRate = 1.5f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
|
||||
float speed = interp.GetMaxSpeed();
|
||||
|
||||
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed()
|
||||
{
|
||||
// WalkForward max speed is always WalkAnimSpeed (3.12) — no run-rate scaling.
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
|
||||
float speed = interp.GetMaxSpeed();
|
||||
|
||||
Assert.Equal(MotionInterpreter.WalkAnimSpeed, speed, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMaxSpeed_WalkBackward_ReturnsWalkAnimSpeedTimesBackwardsFactor()
|
||||
{
|
||||
// BackwardsFactor = 0.65, from adjust_motion @ 0x00528010 in the named retail decomp.
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkBackward;
|
||||
|
||||
float speed = interp.GetMaxSpeed();
|
||||
|
||||
Assert.Equal(MotionInterpreter.WalkAnimSpeed * 0.65f, speed, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMaxSpeed_Idle_ReturnsZero()
|
||||
{
|
||||
// Ready / non-locomotion commands → 0 (no movement speed).
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
|
||||
|
||||
float speed = interp.GetMaxSpeed();
|
||||
|
||||
Assert.Equal(0f, speed, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMaxSpeed_RunForward_NoWeenie_FallsBackToMyRunRate()
|
||||
{
|
||||
// WeenieObj is null (MakeInterp with no weenie argument); MyRunRate
|
||||
// is set explicitly. GetMaxSpeed must use MyRunRate as the run-rate
|
||||
// source when InqRunRate is unavailable.
|
||||
var interp = MakeInterp();
|
||||
interp.MyRunRate = 1.75f;
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
|
||||
float speed = interp.GetMaxSpeed();
|
||||
|
||||
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.75f, speed, precision: 4);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PositionManagerTests — 6 tests covering ComputeOffset.
|
||||
//
|
||||
// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730).
|
||||
// Pure-function combiner: animation root motion (seqVel × dt, rotated by
|
||||
// body orientation) + InterpolationManager.AdjustOffset correction.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public sealed class PositionManagerTests
|
||||
{
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static PositionManager Make() => new();
|
||||
|
||||
private static InterpolationManager EmptyInterp() => new();
|
||||
|
||||
// =========================================================================
|
||||
// Test 1: stationary remote — both sources zero, no motion
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, offset);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 2: animation only, identity orientation, forward velocity
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_AnimationOnly_Forward_BodyAdvances()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
||||
// seqVel = (0, 4, 0), dt = 0.1 → rootMotion = (0, 0.4, 0)
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
||||
Assert.Equal(0f, offset.X, precision: 4);
|
||||
Assert.Equal(0.4f, offset.Y, precision: 4);
|
||||
Assert.Equal(0f, offset.Z, precision: 4);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 3: animation only, 180° yaw around Z — body moves south (-Y)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
||||
// 180° around Z flips +Y → -Y
|
||||
Quaternion ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI);
|
||||
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
ori: ori,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
||||
Assert.Equal(0f, offset.X, precision: 4);
|
||||
Assert.Equal(-0.4f, offset.Y, precision: 4);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 4: interp only, no animation — body chases queue
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = new InterpolationManager();
|
||||
|
||||
// Enqueue target 1m ahead on +X; body starts at origin
|
||||
interp.Enqueue(new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false);
|
||||
|
||||
// Expected catch-up: catchUpSpeed = maxSpeed × 2 = 4 × 2 = 8 m/s
|
||||
// step = 8 × 0.1 = 0.8m (< dist = 1m so no overshoot clamp)
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
||||
Assert.Equal(0.8f, offset.X, precision: 3);
|
||||
Assert.Equal(0f, offset.Y, precision: 3);
|
||||
Assert.Equal(0f, offset.Z, precision: 3);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 5: both sources active — combined delta
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_BothActive_Combined()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = new InterpolationManager();
|
||||
|
||||
// Enqueue target 1m ahead on +X
|
||||
interp.Enqueue(new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false);
|
||||
|
||||
// rootMotion = (0, 4, 0) × 0.1 = (0, 0.4, 0)
|
||||
// correction ≈ (0.8, 0, 0)
|
||||
// combined ≈ (0.8, 0.4, 0)
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
||||
Assert.Equal(0.8f, offset.X, precision: 3);
|
||||
Assert.Equal(0.4f, offset.Y, precision: 3);
|
||||
Assert.Equal(0f, offset.Z, precision: 3);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Test 6: local-to-world rotation — +90° yaw around Z
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_LocalToWorldRotation_Yaw90()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
||||
// +90° CCW around Z in right-handed coordinates:
|
||||
// body-local +Y → world -X
|
||||
Quaternion ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
|
||||
|
||||
// seqVel = (0, 1, 0), dt = 1 → rootMotionLocal = (0, 1, 0)
|
||||
// after Transform by ori → (-1, 0, 0) approximately
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 1.0,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 1f, 0f),
|
||||
ori: ori,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
||||
Assert.Equal(-1f, offset.X, precision: 4);
|
||||
Assert.Equal(0f, offset.Y, precision: 4);
|
||||
Assert.Equal(0f, offset.Z, precision: 4);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
$cdb = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"
|
||||
$script = "C:\Users\erikn\source\repos\acdream\tools\cdb-scripts\walk_run_motion_trace.cdb"
|
||||
$log = "C:\Users\erikn\source\repos\acdream\tools\cdb-scripts\walk_run_motion_trace.log"
|
||||
|
||||
if (Test-Path $log) { Remove-Item $log }
|
||||
|
||||
Write-Host "Attaching cdb to acclient.exe..."
|
||||
Write-Host "Once attached, do this in retail:"
|
||||
Write-Host " 1. Stand still 2s"
|
||||
Write-Host " 2. Hold shift+W (walk) 4s"
|
||||
Write-Host " 3. Release SHIFT only, keep W, 4s (this is what we want to capture)"
|
||||
Write-Host " 4. Release W"
|
||||
Write-Host " 5. Wait for cdb to detach (or Ctrl+C this PS to detach manually)"
|
||||
Write-Host ""
|
||||
|
||||
& $cdb -pn acclient.exe -cf $script *>&1 | Tee-Object -FilePath "$log.console"
|
||||
|
|
@ -1,559 +0,0 @@
|
|||
Opened log file 'C:\Users\erikn\source\repos\acdream\tools\cdb-scripts\walk_run_motion_trace.log'
|
||||
0:017> .sympath C:\Users\erikn\source\repos\acdream\refs
|
||||
Symbol search path is: C:\Users\erikn\source\repos\acdream\refs
|
||||
Expanded Symbol search path is: c:\users\erikn\source\repos\acdream\refs
|
||||
|
||||
************* Path validation summary **************
|
||||
Response Time (ms) Location
|
||||
OK C:\Users\erikn\source\repos\acdream\refs
|
||||
0:017> .symopt+ 0x40
|
||||
Symbol options are 0xB0367:
|
||||
0x00000001 - SYMOPT_CASE_INSENSITIVE
|
||||
0x00000002 - SYMOPT_UNDNAME
|
||||
0x00000004 - SYMOPT_DEFERRED_LOADS
|
||||
0x00000020 - SYMOPT_OMAP_FIND_NEAREST
|
||||
0x00000040 - SYMOPT_LOAD_ANYTHING
|
||||
0x00000100 - SYMOPT_NO_UNQUALIFIED_LOADS
|
||||
0x00000200 - SYMOPT_FAIL_CRITICAL_ERRORS
|
||||
0x00010000 - SYMOPT_AUTO_PUBLICS
|
||||
0x00020000 - SYMOPT_NO_IMAGE_SEARCH
|
||||
0x00080000 - SYMOPT_NO_PROMPTS
|
||||
0:017> .reload /f acclient.exe
|
||||
0:017>
|
||||
0:017> r $t0 = 0
|
||||
0:017>
|
||||
0:017> bp acclient!CPhysicsObj::DoInterpretedMotion ".printf \"\\n[%d] CPhysicsObj::DoInterpretedMotion: motion=%08x speedBits=%08x\", @$t0, poi(esp+4), poi(esp+8); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 0 redefined
|
||||
0:017> bp acclient!CPartArray::DoInterpretedMotion ".printf \"\\n[%d] CPartArray::DoInterpretedMotion: motion=%08x speedBits=%08x\", @$t0, poi(esp+4), poi(esp+8); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 1 redefined
|
||||
0:017> bp acclient!MotionTableManager::PerformMovement ".printf \"\\n[%d] MotionTableManager::PerformMovement: motion=%08x speedBits=%08x holdkey=%08x\", @$t0, poi(esp+4), poi(esp+8), poi(esp+0xc); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 2 redefined
|
||||
0:017> bp acclient!MotionTableManager::add_to_queue ".printf \"\\n[%d] MotionTableManager::add_to_queue: arg1=%08x arg2=%08x\", @$t0, poi(esp+4), poi(esp+8); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 3 redefined
|
||||
0:017> bp acclient!MotionTableManager::truncate_animation_list ".printf \"\\n[%d] MotionTableManager::truncate_animation_list\", @$t0; r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 4 redefined
|
||||
0:017> bp acclient!CMotionTable::DoObjectMotion ".printf \"\\n[%d] CMotionTable::DoObjectMotion: motion=%08x\", @$t0, poi(esp+4); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 5 redefined
|
||||
0:017> bp acclient!CMotionTable::StopObjectMotion ".printf \"\\n[%d] CMotionTable::StopObjectMotion: motion=%08x\", @$t0, poi(esp+4); r $t0 = @$t0 + 1; .if (@$t0 >= 200) { .detach } .else { gc }"
|
||||
0:017>
|
||||
breakpoint 6 redefined
|
||||
0:017> g
|
||||
|
||||
[0] MotionTableManager::PerformMovement: motion=001afc60 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[1] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[2] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[3] MotionTableManager::PerformMovement: motion=001afc2c speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[4] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[5] MotionTableManager::PerformMovement: motion=001afbe4 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[6] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[7] CPhysicsObj::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[8] CPartArray::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[9] MotionTableManager::PerformMovement: motion=001afc14 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[10] CMotionTable::DoObjectMotion: motion=6500000d
|
||||
[11] MotionTableManager::add_to_queue: arg1=6500000d arg2=00000000
|
||||
[12] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[13] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[14] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[15] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[16] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[17] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[18] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[19] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[20] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[21] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[22] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[23] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[24] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[25] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[26] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[27] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[28] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[29] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[30] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[31] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[32] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[33] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[34] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[35] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[36] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[37] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[38] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[39] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[40] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001af5e8
|
||||
[41] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001af5e8
|
||||
[42] MotionTableManager::PerformMovement: motion=001af558 speedBits=15f356a0 holdkey=13cf1420
|
||||
[43] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[44] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[45] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001af5e8
|
||||
[46] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001af5e8
|
||||
[47] MotionTableManager::PerformMovement: motion=001af558 speedBits=15f356a0 holdkey=13cf1420
|
||||
[48] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[49] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[50] MotionTableManager::PerformMovement: motion=001af558 speedBits=15f356a0 holdkey=13cf1420
|
||||
[51] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[52] MotionTableManager::PerformMovement: motion=001af574 speedBits=15f356a0 holdkey=13cf1420
|
||||
[53] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[54] MotionTableManager::PerformMovement: motion=001afc60 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[55] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[56] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[57] MotionTableManager::PerformMovement: motion=001afc2c speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[58] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[59] MotionTableManager::PerformMovement: motion=001afbe4 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[60] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[61] CPhysicsObj::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[62] CPartArray::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[63] MotionTableManager::PerformMovement: motion=001afc14 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[64] CMotionTable::DoObjectMotion: motion=6500000d
|
||||
[65] MotionTableManager::add_to_queue: arg1=6500000d arg2=00000000
|
||||
[66] MotionTableManager::PerformMovement: motion=001af408 speedBits=15f356a0 holdkey=13cf1420
|
||||
[67] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[68] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001af33c
|
||||
[69] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001af33c
|
||||
[70] MotionTableManager::PerformMovement: motion=001af2a0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[71] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[72] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[73] MotionTableManager::PerformMovement: motion=001af2a0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[74] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[75] MotionTableManager::PerformMovement: motion=001af2a0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[76] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[77] MotionTableManager::PerformMovement: motion=001af2a0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[78] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[79] CPhysicsObj::DoInterpretedMotion: motion=45000005 speedBits=001af348
|
||||
[80] CPartArray::DoInterpretedMotion: motion=45000005 speedBits=001af348
|
||||
[81] MotionTableManager::PerformMovement: motion=001af2ac speedBits=15f356a0 holdkey=13cf1420
|
||||
[82] CMotionTable::DoObjectMotion: motion=45000005
|
||||
[83] MotionTableManager::add_to_queue: arg1=45000005 arg2=00000001
|
||||
[84] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001af5e0
|
||||
[85] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001af5e0
|
||||
[86] MotionTableManager::PerformMovement: motion=001af550 speedBits=15f356a0 holdkey=13cf1420
|
||||
[87] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[88] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[89] CPhysicsObj::DoInterpretedMotion: motion=44000007 speedBits=001af5e0
|
||||
[90] CPartArray::DoInterpretedMotion: motion=44000007 speedBits=001af5e0
|
||||
[91] MotionTableManager::PerformMovement: motion=001af550 speedBits=15f356a0 holdkey=13cf1420
|
||||
[92] CMotionTable::DoObjectMotion: motion=44000007
|
||||
[93] MotionTableManager::add_to_queue: arg1=44000007 arg2=00000001
|
||||
[94] MotionTableManager::PerformMovement: motion=001af550 speedBits=15f356a0 holdkey=13cf1420
|
||||
[95] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[96] MotionTableManager::PerformMovement: motion=001af56c speedBits=15f356a0 holdkey=13cf1420
|
||||
[97] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[98] MotionTableManager::PerformMovement: motion=001afcfc speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[99] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[100] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[101] MotionTableManager::PerformMovement: motion=001afc84 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[102] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[103] MotionTableManager::PerformMovement: motion=001af2c0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[104] CMotionTable::StopObjectMotion: motion=44000007
|
||||
[105] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000001
|
||||
[106] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001af4bc
|
||||
[107] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001af4bc
|
||||
[108] MotionTableManager::PerformMovement: motion=001af420 speedBits=15f356a0 holdkey=13cf1420
|
||||
[109] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[110] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[111] MotionTableManager::PerformMovement: motion=001af420 speedBits=15f356a0 holdkey=13cf1420
|
||||
[112] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[113] MotionTableManager::PerformMovement: motion=001af420 speedBits=15f356a0 holdkey=13cf1420
|
||||
[114] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[115] MotionTableManager::PerformMovement: motion=001af420 speedBits=15f356a0 holdkey=13cf1420
|
||||
[116] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[117] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001af47c
|
||||
[118] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001af47c
|
||||
[119] MotionTableManager::PerformMovement: motion=001af3e0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[120] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[121] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[122] MotionTableManager::PerformMovement: motion=001af3e0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[123] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[124] MotionTableManager::PerformMovement: motion=001af3e0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[125] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[126] MotionTableManager::PerformMovement: motion=001af3e0 speedBits=15f356a0 holdkey=13cf1420
|
||||
[127] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[128] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[129] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[130] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[131] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[132] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[133] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[134] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[135] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[136] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[137] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[138] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[139] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[140] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[141] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[142] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[143] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[144] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[145] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[146] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[147] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[148] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[149] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[150] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[151] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[152] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[153] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[154] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[155] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[156] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[157] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[158] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[159] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[160] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[161] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[162] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[163] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[164] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[165] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[166] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[167] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[168] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[169] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[170] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[171] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[172] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[173] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[174] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[175] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[176] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[177] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[178] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[179] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[180] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[181] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[182] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[183] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[184] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[185] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[186] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[187] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[188] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[189] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[190] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[191] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[192] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[193] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[194] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[195] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[196] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[197] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[198] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[199] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[200] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[201] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[202] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[203] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[204] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[205] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[206] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[207] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[208] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[209] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[210] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[211] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[212] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[213] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[214] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[215] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[216] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[217] CPhysicsObj::DoInterpretedMotion: motion=10000052 speedBits=001afcb4
|
||||
[218] CPartArray::DoInterpretedMotion: motion=10000052 speedBits=001afcb4
|
||||
[219] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[220] CMotionTable::DoObjectMotion: motion=10000052
|
||||
[221] MotionTableManager::add_to_queue: arg1=10000052 arg2=00000001
|
||||
[222] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[223] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[224] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[225] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[226] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[227] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[228] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[229] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[230] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[231] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[232] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[233] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[234] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[235] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[236] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[237] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[238] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[239] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[240] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[241] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[242] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[243] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[244] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[245] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[246] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[247] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[248] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[249] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[250] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[251] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[252] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[253] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[254] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[255] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[256] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[257] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[258] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[259] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[260] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[261] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[262] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[263] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[264] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[265] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[266] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[267] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[268] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[269] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[270] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[271] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[272] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[273] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[274] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[275] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[276] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[277] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[278] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[279] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[280] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[281] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[282] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[283] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[284] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[285] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[286] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[287] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[288] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[289] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[290] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[291] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[292] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[293] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[294] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[295] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[296] MotionTableManager::PerformMovement: motion=001afbe4 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[297] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[298] CPhysicsObj::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[299] CPartArray::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[300] MotionTableManager::PerformMovement: motion=001afc14 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[301] CMotionTable::DoObjectMotion: motion=6500000d
|
||||
[302] MotionTableManager::add_to_queue: arg1=6500000d arg2=00000000
|
||||
[303] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[304] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[305] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[306] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[307] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[308] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[309] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[310] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[311] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[312] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[313] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[314] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[315] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[316] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[317] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[318] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[319] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[320] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[321] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[322] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[323] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[324] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[325] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[326] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[327] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[328] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[329] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[330] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[331] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[332] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[333] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[334] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[335] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[336] CPhysicsObj::DoInterpretedMotion: motion=10000052 speedBits=001afcb4
|
||||
[337] CPartArray::DoInterpretedMotion: motion=10000052 speedBits=001afcb4
|
||||
[338] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[339] CMotionTable::DoObjectMotion: motion=10000052
|
||||
[340] MotionTableManager::add_to_queue: arg1=10000052 arg2=00000001
|
||||
[341] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[342] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[343] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[344] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[345] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[346] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[347] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[348] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[349] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[350] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[351] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[352] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[353] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[354] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[355] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[356] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[357] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5ec78 holdkey=16fa4498
|
||||
[358] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[359] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[360] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[361] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[362] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[363] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[364] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[365] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[366] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[367] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[368] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[369] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[370] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[371] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[372] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[373] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[374] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[375] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[376] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[377] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[378] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[379] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[380] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[381] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[382] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[383] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[384] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[385] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[386] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[387] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[388] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[389] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[390] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[391] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[392] CPhysicsObj::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[393] CPartArray::DoInterpretedMotion: motion=10000054 speedBits=001afcb4
|
||||
[394] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[395] CMotionTable::DoObjectMotion: motion=10000054
|
||||
[396] MotionTableManager::add_to_queue: arg1=10000054 arg2=00000001
|
||||
[397] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[398] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[399] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[400] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[401] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[402] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[403] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[404] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[405] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[406] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[407] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[408] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[409] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[410] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[411] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[412] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[413] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[414] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[415] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[416] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[417] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[418] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[419] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[420] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[421] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[422] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[423] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[424] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[425] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e978 holdkey=16fa3328
|
||||
[426] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[427] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e978 holdkey=16fa3328
|
||||
[428] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[429] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[430] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[431] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[432] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[433] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[434] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[435] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[436] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[437] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[438] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[439] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[440] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[441] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e9f8 holdkey=16fa6538
|
||||
[442] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[443] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[444] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[445] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[446] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[447] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[448] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[449] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[450] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[451] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[452] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[453] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[454] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[455] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[456] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[457] MotionTableManager::PerformMovement: motion=001afc60 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[458] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[459] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[460] MotionTableManager::PerformMovement: motion=001afc2c speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[461] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[462] MotionTableManager::PerformMovement: motion=001afbe4 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[463] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[464] CPhysicsObj::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[465] CPartArray::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[466] MotionTableManager::PerformMovement: motion=001afc14 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[467] CMotionTable::DoObjectMotion: motion=6500000d
|
||||
[468] MotionTableManager::add_to_queue: arg1=6500000d arg2=00000000
|
||||
[469] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[470] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[471] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[472] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[473] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[474] CPhysicsObj::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[475] CPartArray::DoInterpretedMotion: motion=41000003 speedBits=001afcb4
|
||||
[476] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[477] CMotionTable::DoObjectMotion: motion=41000003
|
||||
[478] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[479] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[480] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[481] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5eb78 holdkey=16fa6a48
|
||||
[482] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[483] MotionTableManager::PerformMovement: motion=001afc60 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[484] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[485] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[486] MotionTableManager::PerformMovement: motion=001afc2c speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[487] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[488] MotionTableManager::PerformMovement: motion=001afbe4 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[489] MotionTableManager::add_to_queue: arg1=41000003 arg2=00000000
|
||||
[490] CPhysicsObj::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[491] CPartArray::DoInterpretedMotion: motion=6500000d speedBits=001afcc0
|
||||
[492] MotionTableManager::PerformMovement: motion=001afc14 speedBits=16f5e1f8 holdkey=16fa5c38
|
||||
[493] CMotionTable::DoObjectMotion: motion=6500000d
|
||||
[494] MotionTableManager::add_to_queue: arg1=6500000d arg2=00000000
|
||||
[495] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[496] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[497] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[498] CMotionTable::DoObjectMotion: motion=8000003d
|
||||
[499] MotionTableManager::add_to_queue: arg1=8000003d arg2=00000000
|
||||
[500] CPhysicsObj::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[501] CPartArray::DoInterpretedMotion: motion=10000053 speedBits=001afcb4
|
||||
[502] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[503] CMotionTable::DoObjectMotion: motion=10000053
|
||||
[504] MotionTableManager::add_to_queue: arg1=10000053 arg2=00000003
|
||||
[505] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[506] CMotionTable::StopObjectMotion: motion=6500000f
|
||||
[507] MotionTableManager::PerformMovement: motion=001afc40 speedBits=16f5e4f8 holdkey=16fa5218
|
||||
[508] CMotionTable::StopObjectMotion: motion=6500000d
|
||||
[509] CPhysicsObj::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[510] CPartArray::DoInterpretedMotion: motion=8000003d speedBits=001afcb4
|
||||
[511] MotionTableManager::PerformMovement: motion=001afc24 speedBits=16f5e4f8 holdkey=16fa5218Detached
|
||||
Binary file not shown.
1
tools/diag-logs/.gitignore
vendored
1
tools/diag-logs/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.log
|
||||
Loading…
Add table
Add a link
Reference in a new issue