docs(L.2b/L.1b): movement wire-parity slice design + 2026-06-26 audits
Imports the two 2026-06-26 movement/animation retail-parity research docs (audit D1-D12 + ACE-vs-2013 command-catalog gap) and adds the design spec for the first implementation slice: dual command catalog (AceModern runtime default + full-extraction Retail2013 conformance) + outbound wire parity (RawMotionState default-difference packing D1, contact|standingLongjump trailing byte D3, retail JumpPack layout D4). Decomp-verbatim, tests-first; D6 motion-interpreter input-state construction explicitly deferred. Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md Phases: L.2b (movement wire/contact authority) + L.1b (command router). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ca94b479bf
commit
33ad231d18
3 changed files with 736 additions and 0 deletions
151
docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md
Normal file
151
docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# ACE vs 2013 Retail Motion Command Gap
|
||||
|
||||
Date: 2026-06-26
|
||||
|
||||
## Why this matters
|
||||
|
||||
The movement/animation parity work cannot use one command catalog blindly.
|
||||
|
||||
The 2013 `acclient.exe` decomp has a `command_ids[0x198]` table and a matching command-name table, but ACE's `MotionCommand` enum is based on a later client catalog. The local DAT motion tables also contain later-client command keys. If AcDream reconstructs incoming ACE wire commands through the 2013 table only, some server-triggered animations will disappear.
|
||||
|
||||
The concrete example is lifestone recall:
|
||||
|
||||
- 2013 retail decomp names `LifestoneRecall` as `0x10000150`.
|
||||
- Current ACE names `LifestoneRecall` as `0x10000153`.
|
||||
- Local DAT `MotionTable` links include both values, but many later recall/offhand commands only exist under the ACE-shifted value.
|
||||
|
||||
## Sources checked
|
||||
|
||||
- 2013 retail decomp:
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `command_ids[0x198]` at `0x007c73e8`
|
||||
- command-name table around `0x008041ec..0x0080444c`
|
||||
- ACE current master:
|
||||
- `https://raw.githubusercontent.com/ACEmulator/ACE/master/Source/ACE.Entity/Enum/MotionCommand.cs`
|
||||
- `https://raw.githubusercontent.com/ACEmulator/ACE/master/Source/ACE.Entity/Enum/CommandMasks.cs`
|
||||
- Local DATs:
|
||||
- `C:\Users\erikn\Documents\Asheron's Call\client_portal.dat`
|
||||
- scanned 436 `MotionTable` records with `Chorizite.DatReaderWriter 2.1.7`
|
||||
- Current AcDream resolver:
|
||||
- `src/AcDream.Core/Physics/MotionCommandResolver.cs`
|
||||
|
||||
## Catalog mismatch
|
||||
|
||||
Parsed inventory:
|
||||
|
||||
- 2013 retail command names parsed: `406`
|
||||
- ACE command names parsed: `409`
|
||||
- Common names: `400`
|
||||
- Common names with different values: `130`
|
||||
|
||||
The important mismatch is a contiguous low-word `+3` shift beginning after the targeting UI block:
|
||||
|
||||
| Name | 2013 retail | ACE current |
|
||||
|---|---:|---:|
|
||||
| `SnowAngelState` | `0x43000115` | `0x43000118` |
|
||||
| `MeditateState` | `0x43000119` | `0x4300011C` |
|
||||
| `Pickup5` | `0x40000133` | `0x40000136` |
|
||||
| `HouseRecall` | `0x10000137` | `0x1000013A` |
|
||||
| `SitState` | `0x4300013A` | `0x4300013D` |
|
||||
| `HaveASeat` | `0x1300014F` | `0x13000152` |
|
||||
| `LifestoneRecall` | `0x10000150` | `0x10000153` |
|
||||
| `MarketplaceRecall` | `0x10000163` | `0x10000166` |
|
||||
| `AllegianceHometownRecall` | `0x1000016E` | `0x10000171` |
|
||||
| `PKArenaRecall` | `0x1000016F` | `0x10000172` |
|
||||
| `OffhandSlashHigh` | `0x10000170` | `0x10000173` |
|
||||
|
||||
ACE's enum comments show the branch point:
|
||||
|
||||
- `SkillHealSelf = 0x1000010e`
|
||||
- `SkillHealOther = 0x1000010f`
|
||||
- duplicate/commented legacy slots around `0x010f..0x0111`
|
||||
- `SnowAngelState = 0x43000118`
|
||||
|
||||
2013 retail instead has:
|
||||
|
||||
- `SkillHealSelf = 0x0900010E`
|
||||
- `NextMonster = 0x0900010F`
|
||||
- `PreviousMonster = 0x09000110`
|
||||
- `ClosestMonster = 0x09000111`
|
||||
- `NextPlayer = 0x09000112`
|
||||
- `PreviousPlayer = 0x09000113`
|
||||
- `ClosestPlayer = 0x09000114`
|
||||
- `SnowAngelState = 0x43000115`
|
||||
|
||||
So this is a later-client catalog divergence, not just a single bad enum value.
|
||||
|
||||
## MotionTable availability
|
||||
|
||||
I scanned all 436 local DAT `MotionTable` records for the selected 2013 and ACE values. Counts below are link target hits; recall/action commands are stored in `Links`, not `Cycles`.
|
||||
|
||||
| Command | 2013 value hits | ACE value hits | Interpretation |
|
||||
|---|---:|---:|---|
|
||||
| `HouseRecall` | `317` | `24` | both exist; the old value is very common |
|
||||
| `LifestoneRecall` | `28` | `19` | both exist; ACE `/ls` value can animate |
|
||||
| `MarketplaceRecall` | `0` | `19` | only ACE-shifted value exists |
|
||||
| `AllegianceHometownRecall` | `0` | `19` | only ACE-shifted value exists |
|
||||
| `PKArenaRecall` | `0` | `18` | only ACE-shifted value exists |
|
||||
| `OffhandSlashHigh` | `0` | `31` | only ACE-shifted value exists |
|
||||
|
||||
This means the local DATs are not pure 2013-command-table data. They include later-client action keys that match ACE/DatReaderWriter.
|
||||
|
||||
## Current AcDream resolver behavior
|
||||
|
||||
Current `MotionCommandResolver.ReconstructFullCommand` results:
|
||||
|
||||
| Wire low | Current full command |
|
||||
|---:|---:|
|
||||
| `0x0137` | `0x40000137` |
|
||||
| `0x013A` | `0x1000013A` |
|
||||
| `0x0150` | `0x13000150` |
|
||||
| `0x0153` | `0x10000153` |
|
||||
| `0x0163` | `0x09000163` |
|
||||
| `0x0166` | `0x10000166` |
|
||||
| `0x016E` | `0x1000016E` |
|
||||
| `0x016F` | `0x1000016F` |
|
||||
| `0x0170` | `0x10000170` |
|
||||
| `0x0171` | `0x10000171` |
|
||||
| `0x0172` | `0x10000172` |
|
||||
| `0x0173` | `0x10000173` |
|
||||
|
||||
Implications:
|
||||
|
||||
- ACE `/ls` wire `0x0153` currently reconstructs to `0x10000153`, which is good for ACE and local DATs.
|
||||
- 2013 retail lifestone wire `0x0150` would currently reconstruct as `ScanHorizon` chat emote, not `LifestoneRecall`.
|
||||
- The override comment in `MotionCommandResolver` is wrong: the mismatch does not start at `AllegianceHometownRecall`; it starts earlier around `SnowAngelState`.
|
||||
- The override range `0x016E..0x0197` maps `0x016E/0x016F/0x0170` to action-class commands even though ACE names those low words as UI commands. This probably does not hurt normal ACE animation broadcasts, but it is not a clean catalog model.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Do not replace ACE/DatReaderWriter reconstruction with 2013 `command_ids` globally.
|
||||
|
||||
Use two catalogs explicitly:
|
||||
|
||||
1. `Retail2013CommandCatalog`
|
||||
- For proving decomp behavior and reproducing the 2013 client command table.
|
||||
- Useful for old-client conformance tests.
|
||||
|
||||
2. `AceModernCommandCatalog`
|
||||
- For reconstructing ACE `InterpretedMotionState` wire `u16` commands into the full 32-bit motion commands that match local DAT motion tables.
|
||||
- Should be the runtime default while talking to ACE.
|
||||
|
||||
Then add a resolver mode/test matrix:
|
||||
|
||||
- ACE mode:
|
||||
- `0x0153 -> 0x10000153` (`LifestoneRecall`)
|
||||
- `0x0166 -> 0x10000166` (`MarketplaceRecall`)
|
||||
- `0x0171 -> 0x10000171` (`AllegianceHometownRecall`)
|
||||
- `0x0173 -> 0x10000173` (`OffhandSlashHigh`)
|
||||
- 2013 retail mode:
|
||||
- `0x0150 -> 0x10000150` (`LifestoneRecall`)
|
||||
- `0x0163 -> 0x10000163` (`MarketplaceRecall`)
|
||||
- `0x016E -> 0x1000016E` (`AllegianceHometownRecall`)
|
||||
- `0x0170 -> 0x10000170` (`OffhandSlashHigh`)
|
||||
|
||||
Animation lookup should be tested against the DAT motion tables too, not just enum names. A command is visually usable only if the entity's `MotionTable` has a `Links` or `Modifiers` entry for that full command.
|
||||
|
||||
## Bottom line
|
||||
|
||||
For ACE interop, the lifestone recall animation is not missing from the local DAT motion tables. It is missing only if we force ACE's wire `0x0153` through the 2013 retail command catalog.
|
||||
|
||||
The gap is real and broader than lifestone recall: the later-client/ACE command catalog diverges from the 2013 decomp for 130 common names, and several important animations only exist under the ACE-shifted IDs in the local DATs.
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
# Movement and Animation Retail Parity Audit
|
||||
|
||||
Date: 2026-06-26
|
||||
|
||||
## Verdict on approach
|
||||
|
||||
The approach is good if it stays decomp-first and test-first. For this area, "verbatim" needs to mean:
|
||||
|
||||
- Outbound wire bytes match retail packers for the same input state.
|
||||
- Incoming movement packets are parsed into the same semantic state retail would unpack.
|
||||
- Local, remote player, monster, NPC, and force-walk paths flow through retail-equivalent motion state transitions before animation selection.
|
||||
- Any behavior that cannot be proven from retail decomp, Ghidra, or retail captures is marked unresolved instead of tuned by feel.
|
||||
|
||||
The approach is not good if it begins as a broad rewrite or animation polish pass. The current code has several comments and tests that already encode approximations. Those need to become failing parity tests first, then implementation slices.
|
||||
|
||||
## Sources checked
|
||||
|
||||
Primary oracle:
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `docs/research/named-retail/acclient.h`
|
||||
- `docs/research/named-retail/acclient.c`
|
||||
|
||||
Secondary oracle:
|
||||
|
||||
- `docs/research/acclient_decompiled.c`
|
||||
- Ghidra HTTP bridge verified on `http://127.0.0.1:8081`; `methods?limit=3` responded and decompile-by-address worked during the audit. Codex tool discovery did not expose a first-class Ghidra MCP namespace in this thread, so direct HTTP is the usable path here.
|
||||
|
||||
Current implementation surfaces:
|
||||
|
||||
- `src/AcDream.Core.Net/Messages/MoveToState.cs`
|
||||
- `src/AcDream.Core.Net/Messages/AutonomousPosition.cs`
|
||||
- `src/AcDream.Core.Net/Messages/JumpAction.cs`
|
||||
- `src/AcDream.Core.Net/Messages/UpdateMotion.cs`
|
||||
- `src/AcDream.Core.Net/Messages/UpdatePosition.cs`
|
||||
- `src/AcDream.Core.Net/Messages/CreateObject.cs`
|
||||
- `src/AcDream.Core.Net/WorldSession.cs`
|
||||
- `src/AcDream.Core/Physics/MotionInterpreter.cs`
|
||||
- `src/AcDream.Core/Physics/AnimationSequencer.cs`
|
||||
- `src/AcDream.Core/Physics/ServerControlledLocomotion.cs`
|
||||
- `src/AcDream.Core/Physics/RemoteMoveToDriver.cs`
|
||||
- `src/AcDream.App/Input/PlayerMovementController.cs`
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs`
|
||||
|
||||
## Retail source map
|
||||
|
||||
### Outbound movement to ACE
|
||||
|
||||
- `CommandInterpreter::ShouldSendPositionEvent` at `0x006b45e0`, pseudo line around `700233`.
|
||||
- `CommandInterpreter::SendMovementEvent` at `0x006b4680`, pseudo line around `700274`.
|
||||
- `CommandInterpreter::SendPositionEvent` at `0x006b4770`, pseudo line around `700316`.
|
||||
- `CM_Movement::Event_AutonomousPosition` at `0x006af790`, sub-opcode `0xf753`.
|
||||
- `CM_Movement::Event_Jump` at `0x006afa70`, sub-opcode `0xf61b`.
|
||||
- `CM_Movement::Event_MoveToState` at `0x006afc00`, sub-opcode `0xf61c`.
|
||||
- `MoveToStatePack::Pack` at `0x005168f0`, pseudo line around `284694`.
|
||||
- `AutonomousPositionPack::Pack` at `0x00516af0`, pseudo line around `284795`.
|
||||
- `JumpPack::Pack` at `0x00516d10`, pseudo line around `284915`.
|
||||
- `RawMotionState::Pack` at `0x0051ed10`, pseudo line around `293761`.
|
||||
|
||||
Retail `RawMotionState` defaults:
|
||||
|
||||
- `current_holdkey = HoldKey.None`
|
||||
- `current_style = 0x8000003d`
|
||||
- `forward_command = 0x41000003`
|
||||
- `forward_holdkey = HoldKey.Invalid`
|
||||
- `forward_speed = 1.0`
|
||||
- `sidestep_command = 0`
|
||||
- `sidestep_holdkey = HoldKey.Invalid`
|
||||
- `sidestep_speed = 1.0`
|
||||
- `turn_command = 0`
|
||||
- `turn_holdkey = HoldKey.Invalid`
|
||||
- `turn_speed = 1.0`
|
||||
|
||||
Retail `RawMotionState::Pack` compares against these defaults. It does not set a field bit merely because the caller supplied a value. For example, `forward_speed == 1.0f` is omitted.
|
||||
|
||||
### Incoming movement and force movement
|
||||
|
||||
- `MovementManager::PerformMovement` at `0x005240d0`, pseudo line around `300194`, routes movement types `1-5` to `CMotionInterp` and `6-9` to `MoveToManager`.
|
||||
- `MovementManager::unpack_movement` at `0x00524440`, pseudo line around `300563`, unpacks interpreted motion plus `MoveToObject`, `MoveToPosition`, `TurnToObject`, and `TurnToHeading`.
|
||||
- `MovementParameters::UnPackNet` at `0x0052ac70`, pseudo line around `308118`.
|
||||
- `MovementParameters::get_command` at `0x0052aa00`, pseudo line around `307946`.
|
||||
- `MoveToManager::_DoMotion`, `MoveToPosition`, and related queue logic are around pseudo lines `306351`, `307187`, and `307521`.
|
||||
|
||||
Retail movement types from `acclient.h`:
|
||||
|
||||
- `0`: interpreted motion in inbound packet context
|
||||
- `1`: raw motion
|
||||
- `2`: interpreted motion
|
||||
- `3`: stop raw motion
|
||||
- `4`: stop interpreted motion
|
||||
- `5`: stop completely
|
||||
- `6`: move to object
|
||||
- `7`: move to position
|
||||
- `8`: turn to object
|
||||
- `9`: turn to heading
|
||||
|
||||
### Animation and motion interpretation
|
||||
|
||||
- `CMotionInterp::apply_run_to_command` at `0x00527be0`, pseudo line around `305062`.
|
||||
- `CMotionInterp::get_state_velocity` at `0x00527d50`.
|
||||
- `CMotionInterp::adjust_motion` at `0x00528010`, pseudo line around `305343`.
|
||||
- `CMotionInterp::apply_raw_movement` at `0x005287e0`.
|
||||
- `CMotionInterp::DoInterpretedMotion` around pseudo line `305575`.
|
||||
- `CMotionInterp::apply_current_movement` around pseudo line `305713`.
|
||||
- `CMotionTable::GetObjectSequence` around pseudo line `298636`.
|
||||
- `CSequence` append/advance/update logic around pseudo lines `301622`, `301777`, `301839`, and `302425`.
|
||||
|
||||
Key retail normalization:
|
||||
|
||||
- `SideStepLeft` becomes `SideStepRight` with negative speed.
|
||||
- `SideStepRight` speed becomes `(3.11999989 / 1.25) * 0.5 * speed`.
|
||||
- `TurnLeft` becomes `TurnRight` with negative speed.
|
||||
- `WalkBackward` becomes `WalkForward` with `-0.649999976 * speed`.
|
||||
- Hold-key `Run` upgrades forward walk to run, multiplies turn speed by `1.5`, and multiplies sidestep by run rate with an absolute clamp of `3.0`.
|
||||
|
||||
## Confirmed divergences
|
||||
|
||||
### D1: MoveToState raw flags are not retail
|
||||
|
||||
Current `MoveToState.Build` sets raw-motion flags from nullable/presence inputs. Retail sets flags by comparing the complete `RawMotionState` against default values. This means the current client can over-send:
|
||||
|
||||
- `CurrentHoldKey = None`
|
||||
- `ForwardSpeed = 1.0`
|
||||
- `SidestepSpeed = 1.0`
|
||||
- `TurnSpeed = 1.0`
|
||||
- likely per-axis hold keys when they are default `Invalid`
|
||||
|
||||
Existing `MoveToStateTests.Build_WalkForward_IncludesForwardCommandInFlags` currently asserts the non-retail behavior by expecting a `ForwardSpeed` flag for speed `1.0`.
|
||||
|
||||
Impact: outbound movement bytes can differ from retail for normal running, walking, sidestepping, and turning.
|
||||
|
||||
### D2: RawMotionState action-list and style packing are incomplete
|
||||
|
||||
Retail bitfield layout uses 11 one-bit flags plus `num_actions : 5` at bits `11-15`. Current comments imply the command list length is broader than retail. Current builder does not cover full action-list packing or current-style scenarios.
|
||||
|
||||
Impact: action/modifier movement states cannot be proven retail-equivalent.
|
||||
|
||||
### D3: MoveToState longjump bit is not modeled
|
||||
|
||||
Retail `MoveToStatePack::Pack` writes one trailing byte:
|
||||
|
||||
- bit `0x01`: contact
|
||||
- bit `0x02`: `standing_longjump`
|
||||
|
||||
Current `GameWindow` passes only contact `0/1` into a generic byte. The source of `standing_longjump` is not wired as a named state.
|
||||
|
||||
Impact: standing longjump movement-state updates can differ from retail.
|
||||
|
||||
### D4: JumpAction packet layout is retail-incompatible
|
||||
|
||||
Retail `JumpPack` packs:
|
||||
|
||||
- `float extent`
|
||||
- `Vector3 velocity`
|
||||
- full `Position`
|
||||
- four `u16` update timestamps
|
||||
- alignment
|
||||
|
||||
Current `JumpAction.Build` packs extent and velocity, then timestamps, then extra `objectGuid` and `spellId`, and does not pack `Position`.
|
||||
|
||||
Impact: local jump movement update is a high-confidence wire mismatch.
|
||||
|
||||
### D5: Position heartbeat is close but not fully proven
|
||||
|
||||
Retail `ShouldSendPositionEvent` gates autonomous position updates by active state, autonomy level, player smartbox, 1.0 second interval, cell/frame change, and contact-plane change. Current `PlayerMovementController` has a similar cadence.
|
||||
|
||||
Open proof point: retail `SendMovementEvent` visibly stamps `last_sent_position_time`; retail `SendPositionEvent` stamps time, position, and contact plane. Current `GameWindow` calls `NotePositionSent` after both MTS and AP, stamping all three.
|
||||
|
||||
Impact: the client may suppress or reorder autonomous position sends differently after movement-state sends.
|
||||
|
||||
### D6: MotionInterpreter lacks canonical retail raw-to-interpreted normalization
|
||||
|
||||
Retail `CMotionInterp::apply_raw_movement` copies raw state, calls `adjust_motion` for forward/sidestep/turn, applies run-hold behavior, then applies interpreted movement. Current `MotionInterpreter.DoMotion` stores commands directly and applies velocity directly.
|
||||
|
||||
Specific visible risks:
|
||||
|
||||
- `SideStepLeft` may produce no velocity because velocity code only recognizes `SideStepRight`.
|
||||
- backward and sidestep speeds are patched elsewhere instead of normalized at the retail source point.
|
||||
- turn run-speed scaling is not faithfully represented.
|
||||
- local jump lateral velocity relies on comments that say this is temporary until `adjust_motion` is ported.
|
||||
|
||||
Impact: running, slow walking, slow side walking, backward movement, turning, and jump lateral motion are not retail-proven.
|
||||
|
||||
### D7: Animation application is split away from retail motion flow
|
||||
|
||||
Retail movement application sequences style, forward or falling, sidestep start/stop, turn start/stop, and actions through `CMotionInterp` and `CMotionTable::GetObjectSequence`. Current `AnimationSequencer` implements useful pieces, but `MotionInterpreter.apply_current_movement` is mostly velocity-oriented and does not drive animation state through the same retail order.
|
||||
|
||||
Impact: animations can look plausible while being state-machine divergent.
|
||||
|
||||
### D8: Force-walk and MoveTo are approximations
|
||||
|
||||
Retail `MoveToManager` is a queued command system over `CMotionInterp`, with pre-turn, move, aux turn, final heading, sticky targeting, progress failure, and `MovementParameters::get_command` walk/run/hold-key selection.
|
||||
|
||||
Current `ServerControlledLocomotion`, `RemoteMoveToDriver`, and `PlayerMovementController.BeginServerAutoWalk` approximate visible steering and cycle selection.
|
||||
|
||||
Impact: ACE force walking and monster/NPC MoveTo behavior are not retail-equivalent.
|
||||
|
||||
### D9: Inbound movement types 8 and 9 are dropped
|
||||
|
||||
Retail inbound movement supports:
|
||||
|
||||
- `8`: `TurnToObject`
|
||||
- `9`: `TurnToHeading`
|
||||
|
||||
Current `UpdateMotion` handles interpreted state and MoveTo types `6/7`, but not `8/9`.
|
||||
|
||||
Impact: incoming server turn commands for characters, monsters, and NPCs are ignored.
|
||||
|
||||
### D10: Spawn-time movement state is weaker than live movement state
|
||||
|
||||
`CreateObject` can detect movement types and some flags, but it does not preserve full MoveTo target/origin/threshold data the way live `UpdateMotion` does.
|
||||
|
||||
Impact: monsters/NPCs spawned while already moving cannot be driven retail-equivalently until a later movement packet arrives.
|
||||
|
||||
### D11: Sequence/autonomy data is parsed then discarded
|
||||
|
||||
Retail carries movement sequence, server-control sequence, autonomous state, motion flags, and position sequence into movement application. Current events expose only a subset.
|
||||
|
||||
Impact: ordering, stale update rejection, and force-control transitions cannot match retail.
|
||||
|
||||
### D12: Jump/falling/contact gates are simplified
|
||||
|
||||
Retail allows specific movement while falling/dead and has separate jump checks for posture, stamina, constraints, pending motion, contact, and leave/hit-ground reapplication. Current code blocks or simplifies several of these paths.
|
||||
|
||||
Impact: airborne movement, falling animation, dead/fallen movement commands, and jump eligibility are not retail-proven.
|
||||
|
||||
## Priority plan
|
||||
|
||||
### Phase 0: Lock the oracle
|
||||
|
||||
1. Create small retail-reference helpers/tests that encode `RawMotionState::Pack`, `MoveToStatePack::Pack`, `AutonomousPositionPack::Pack`, `JumpPack::Pack`, `MovementParameters::UnPackNet`, and `MovementParameters::get_command`.
|
||||
2. Every helper must cite the decomp function and address in the test name or comments.
|
||||
3. Any unclear branch gets a TODO with address and blocked status, not an approximation.
|
||||
|
||||
Exit criteria: failing tests describe current divergences without changing runtime behavior yet.
|
||||
|
||||
### Phase 1: Fix outbound wire parity
|
||||
|
||||
1. Change `MoveToState` packing from presence-based to retail default-difference packing.
|
||||
2. Add explicit raw-state model with defaults, current style, action list count, and per-axis hold keys.
|
||||
3. Add explicit `contact` and `standingLongjump` parameters for MTS trailing byte.
|
||||
4. Replace `JumpAction` with retail `JumpPack`: extent, velocity, full position, four timestamps, align; remove extra object/spell fields unless another retail opcode proves they belong elsewhere.
|
||||
5. Verify timestamp order: instance `[8]`, server-control `[5]`, teleport `[4]`, force-position `[6]`.
|
||||
6. Audit whether MTS should stamp only last-send time or also last-send position/contact plane.
|
||||
|
||||
Exit criteria: golden byte tests pass for walk, run, sidestep, turn, slow walk/toggle-run, jump, contact, longjump, and AP heartbeat cases.
|
||||
|
||||
### Phase 2: Port retail raw/interpreted motion core
|
||||
|
||||
1. Introduce retail-shaped `RawMotionState` and `InterpretedMotionState` in core physics.
|
||||
2. Port `adjust_motion`, `apply_run_to_command`, `get_state_velocity`, and `apply_raw_movement`.
|
||||
3. Route local player input through this path instead of compensating in `PlayerMovementController`.
|
||||
4. Preserve run rate and hold-key semantics, including toggle-run slow-walk behavior.
|
||||
|
||||
Exit criteria: movement state tests prove backward, sidestep left/right, run turn, run sidestep clamp, forward run, and slow-walk semantics match retail math.
|
||||
|
||||
### Phase 3: Reconnect animation to retail movement application
|
||||
|
||||
1. Port enough of `CMotionInterp::apply_current_movement` to sequence style, forward/falling, sidestep stop/start, turn stop/start, and actions in retail order.
|
||||
2. Keep existing `AnimationSequencer` where it already matches retail, but move normalization out of sequencer-only code and into motion interpretation.
|
||||
3. Add parity tests for reverse playback, link fallback, modifier state, action sequencing, and root-motion composition using real retail motion table data.
|
||||
|
||||
Exit criteria: local and remote animation selection flows from the same interpreted state that drives velocity.
|
||||
|
||||
### Phase 4: Port inbound movement and force-walk behavior
|
||||
|
||||
1. Extend `UpdateMotion` to parse and surface types `8` and `9`.
|
||||
2. Preserve sequence/autonomy/motion flags through `WorldSession` events.
|
||||
3. Preserve full spawn-time MoveTo state in `CreateObject`.
|
||||
4. Replace approximate server-driven locomotion with a retail-shaped `MoveToManager` queue over `CMotionInterp`.
|
||||
5. Port `MovementParameters::get_command`, including walk/run threshold, `can_run`, force-walk, hold-key application, fail distance, sticky/target following, and final heading.
|
||||
|
||||
Exit criteria: incoming interpreted motion, MoveToObject, MoveToPosition, TurnToObject, TurnToHeading, and ACE force-walk packets animate and move through retail-equivalent state transitions.
|
||||
|
||||
### Phase 5: Integration captures and regression guard
|
||||
|
||||
1. Capture retail-equivalent fixtures for normal run, slow walk, sidestep, slow sidestep, backward, turn, jump, forced walk, NPC MoveTo, monster chase/flee, and remote player interpolation.
|
||||
2. Assert wire bytes for outbound messages.
|
||||
3. Assert parsed semantic state for inbound messages.
|
||||
4. Assert animation command sequence and velocity for local and remote entities.
|
||||
5. Run full test suite plus a targeted in-app smoke pass.
|
||||
|
||||
Exit criteria: no movement or animation path is accepted without a retail-addressed test.
|
||||
|
||||
## Initial test inventory to add or replace
|
||||
|
||||
- `MoveToState` golden tests: default raw state, walk forward speed `1.0`, run forward, backward, sidestep right/left, turn right/left, current style, action count, contact/longjump byte.
|
||||
- `JumpAction` golden tests: retail `JumpPack` layout with full position and no object/spell fields.
|
||||
- `AutonomousPosition` tests: timestamp order and contact byte.
|
||||
- `PlayerMovementController` tests: forward walk/run, slow walk/toggle-run, backward, sidestep, turn-only run, jump release, server auto-walk suppression.
|
||||
- `ShouldSendPositionEvent` tests: pre-interval cell change, pre-interval contact-plane change, interval frame change, idle no-send, airborne AP suppression.
|
||||
- `UpdateMotion` tests: interpreted, MoveToObject, MoveToPosition, TurnToObject, TurnToHeading.
|
||||
- `CreateObject` tests: spawn-time MoveTo preservation.
|
||||
- `WorldSession` tests: sequence/autonomy propagation.
|
||||
- `MotionInterpreter` tests: `adjust_motion`, `apply_run_to_command`, `get_state_velocity`, contact gates, falling fallback, jump eligibility.
|
||||
- `AnimationSequencer` tests: retail sequence order, stop/start transitions, reverse playback, link fallback, modifier combine/subtract behavior.
|
||||
- `MoveToManager` tests: pre-turn, move, aux turn, final heading, target following, fail distance, force-walk walk/run choice.
|
||||
|
||||
## Implementation rule
|
||||
|
||||
No code path in this system should remain justified by "feels like retail", "ACE-compatible", or "close enough". If the decomp is unclear, use Ghidra or captures to resolve it. If it still cannot be resolved, leave the behavior behind a clearly named unresolved test or blocked TODO rather than guessing.
|
||||
Loading…
Add table
Add a link
Reference in a new issue