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.
|
||||||
284
docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md
Normal file
284
docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
# Movement / Animation Wire-Parity Slice — Design
|
||||||
|
|
||||||
|
Date: 2026-06-30
|
||||||
|
Phase: **L.2b** (movement wire/contact authority) + **L.1b** (command router + motion-state cleanup)
|
||||||
|
Standard: **decomp-verbatim, tests-first. No approximations, no "feels like retail."**
|
||||||
|
|
||||||
|
## Source documents (read first)
|
||||||
|
|
||||||
|
- `docs/research/2026-06-26-movement-animation-retail-parity-audit.md` — the 12
|
||||||
|
confirmed divergences (D1–D12) and the Phase 0–5 plan.
|
||||||
|
- `docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md` — the ACE-vs-2013
|
||||||
|
command-catalog divergence (130 common names with different values; the `+3`
|
||||||
|
low-word shift beginning at `SnowAngelState`).
|
||||||
|
- Oracle: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR
|
||||||
|
build) + `docs/research/named-retail/acclient.h`. Live confirmation via the
|
||||||
|
Ghidra HTTP bridge at `http://127.0.0.1:8081` (decompile-by-address).
|
||||||
|
- Plans of record this slice serves: `docs/plans/animation-system-audit.md` (L.1)
|
||||||
|
and `docs/plans/2026-04-29-movement-collision-conformance.md` (L.2).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the **outbound movement wire bytes** (`MoveToState 0xF61C`, `Jump 0xF61B`,
|
||||||
|
`AutonomousPosition 0xF753`) and the **inbound→animation command catalog**
|
||||||
|
byte-for-byte / value-for-value faithful to retail, proven by tests whose golden
|
||||||
|
values are derived directly from the 2013 decomp packers and cross-checked
|
||||||
|
against holtburger.
|
||||||
|
|
||||||
|
This slice fixes the *packing* and the *catalog*. It does **not** port the
|
||||||
|
motion-interpreter input-state construction (`adjust_motion` /
|
||||||
|
`apply_raw_movement`) — that is the next slice (D6 / audit Phase 2), explicitly
|
||||||
|
deferred below.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- **D1** — `RawMotionState::Pack` default-difference flag packing (replaces
|
||||||
|
presence-based flags).
|
||||||
|
- **D3** — `MoveToStatePack` trailing byte = `contact | standingLongjump`.
|
||||||
|
- **D4** — `JumpPack` layout (extent, velocity, full Position, four timestamps;
|
||||||
|
drop the spurious objectGuid/spellId).
|
||||||
|
- **Catalog gap** — dual command catalog (`AceModernCommandCatalog` runtime
|
||||||
|
default + `Retail2013CommandCatalog` conformance), built behind an
|
||||||
|
`IMotionCommandCatalog` seam; full 2013 `command_ids[0x198]` extraction;
|
||||||
|
DAT-availability tests.
|
||||||
|
- **Phase 0 oracle tests** — written first and failing, encoding the four retail
|
||||||
|
`Pack` functions and the catalog matrices.
|
||||||
|
|
||||||
|
### Out of scope (later L.1/L.2 slices — see "Deferred / unresolved")
|
||||||
|
D2 (runtime actions-list/current_style emission), D5 (heartbeat-clock stamping —
|
||||||
|
*audit only* this slice), D6 (`adjust_motion`/`apply_raw_movement` input-state),
|
||||||
|
D7 (animation/motion application reorder), D8 (MoveToManager/force-walk), D9
|
||||||
|
(inbound types 8/9), D10 (spawn-time MoveTo), D11 (sequence/autonomy
|
||||||
|
propagation), D12 (jump/contact gates).
|
||||||
|
|
||||||
|
## Decisions (locked with the user 2026-06-30)
|
||||||
|
|
||||||
|
1. **First slice = catalog + outbound wire** (not catalog-only, not + motion core).
|
||||||
|
2. **Golden-byte oracle = decomp-derived + holtburger cross-check**, confirmed via
|
||||||
|
the Ghidra bridge. No live retail capture for this slice (captures are reserved
|
||||||
|
for Phase 2, where retail's *input state* is the genuine unknown).
|
||||||
|
3. **Catalog mechanism = Option A** — `IMotionCommandCatalog` interface; static
|
||||||
|
`MotionCommandResolver.ReconstructFullCommand(ushort)` is retained, delegating
|
||||||
|
to an `AceModernCommandCatalog` singleton so all ~10 runtime call sites are
|
||||||
|
unchanged. Tests instantiate `Retail2013CommandCatalog` explicitly.
|
||||||
|
4. **2013 table depth = full extraction** (~408 `command_ids[0x198]` entries with
|
||||||
|
provenance), not a test-matrix stub.
|
||||||
|
|
||||||
|
## Component design
|
||||||
|
|
||||||
|
### 1. Command catalog (L.1b)
|
||||||
|
|
||||||
|
**Seam.** New interface in `AcDream.Core.Physics`:
|
||||||
|
|
||||||
|
```
|
||||||
|
public interface IMotionCommandCatalog
|
||||||
|
{
|
||||||
|
/// Reconstruct the full 32-bit MotionCommand from a 16-bit wire value.
|
||||||
|
/// Returns 0 if no entry matches.
|
||||||
|
uint ReconstructFullCommand(ushort wireCommand);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`AceModernCommandCatalog : IMotionCommandCatalog`** — the runtime default.
|
||||||
|
Built from the DatReaderWriter / ACE `MotionCommand` enum (these share values;
|
||||||
|
ACE's enum is the authoritative ACE-modern catalog and matches the local DATs).
|
||||||
|
Reconstruction = wire-low → full lookup, with collisions resolved by the
|
||||||
|
documented retail class priority (lower class byte wins: Action 0x10 < SubState
|
||||||
|
0x41 < Style 0x80 …) applied **at build time**, producing a flat
|
||||||
|
`IReadOnlyDictionary<ushort,uint>`.
|
||||||
|
|
||||||
|
- **Verify and (likely) delete the magic override.** The current
|
||||||
|
`MotionCommandResolver.ApplyNamedRetailOverrides` force-maps `0x016E–0x0197`
|
||||||
|
to `0x10000000 | lo`. Once `AceModernCommandCatalog` is built cleanly from the
|
||||||
|
ACE enum, assert the ACE test matrix passes (`0x0153→0x10000153`,
|
||||||
|
`0x0166→0x10000166`, `0x0171→0x10000171`, `0x0173→0x10000173`). If it passes
|
||||||
|
**without** the override loop, delete the loop (unexplained magic range; see
|
||||||
|
the magic-number-audit memory). If a specific command still resolves to the
|
||||||
|
wrong class, add a **per-command, cited** override — never a blind range.
|
||||||
|
- **Fix the wrong comment.** The shift starts at `SnowAngelState` (`0x43000115`
|
||||||
|
→ `0x43000118`), not `AllegianceHometownRecall`.
|
||||||
|
|
||||||
|
**`Retail2013CommandCatalog : IMotionCommandCatalog`** — conformance/reference.
|
||||||
|
Built from the **full** 2013 `command_ids[0x198]` table at `0x007c73e8` paired
|
||||||
|
with the command-name table at `0x008041ec..0x0080444c`. Deterministic
|
||||||
|
extraction from `acclient_2013_pseudo_c.txt` (and/or the Ghidra bridge), emitted
|
||||||
|
as a generated/static name→value table in the source with a provenance header
|
||||||
|
citing both addresses. ~408 entries.
|
||||||
|
|
||||||
|
**`MotionCommandResolver`** keeps `public static uint ReconstructFullCommand(ushort)`
|
||||||
|
delegating to a private `static readonly AceModernCommandCatalog s_aceModern`.
|
||||||
|
All current callers (`AnimationCommandRouter`, `CombatAnimationPlanner`, 8×
|
||||||
|
`GameWindow`) are untouched.
|
||||||
|
|
||||||
|
**DAT-availability tests.** A command animates only if the entity's `MotionTable`
|
||||||
|
has a `Links` or `Modifiers` entry for the full 32-bit value. Tests
|
||||||
|
(`MotionCommandCatalogTests`, real-DAT-backed via `Chorizite.DatReaderWriter`):
|
||||||
|
- ACE mode: `0x0153→0x10000153 LifestoneRecall`, `0x0166→0x10000166`,
|
||||||
|
`0x0171→0x10000171`, `0x0173→0x10000173`.
|
||||||
|
- 2013 mode: `0x0150→0x10000150 LifestoneRecall`, `0x0163→0x10000163`,
|
||||||
|
`0x016E→0x1000016E`, `0x0170→0x10000170`.
|
||||||
|
- Availability: `LifestoneRecall (ACE 0x10000153)` and `HouseRecall` exist in
|
||||||
|
local DAT MotionTables; `MarketplaceRecall / AllegianceHometownRecall /
|
||||||
|
PKArenaRecall / OffhandSlashHigh` exist **only** under the ACE-shifted IDs
|
||||||
|
(reproduces the gap doc's link-hit scan as a fixture assertion).
|
||||||
|
|
||||||
|
### 2. RawMotionState model + default-difference packing (D1)
|
||||||
|
|
||||||
|
**Data type** `AcDream.Core.Physics.RawMotionState` — a value type mirroring the
|
||||||
|
retail struct (`acclient.h RawMotionState::PackBitfield`, decomp `0x0051ed10`):
|
||||||
|
|
||||||
|
| field | retail default | bit |
|
||||||
|
|---|---|---:|
|
||||||
|
| current_holdkey | `HoldKey.None` (1) | 0x001 |
|
||||||
|
| current_style | `0x8000003D` | 0x002 |
|
||||||
|
| forward_command | `0x41000003` | 0x004 |
|
||||||
|
| forward_holdkey | `HoldKey.Invalid` (0) | 0x008 |
|
||||||
|
| forward_speed | `1.0f` | 0x010 |
|
||||||
|
| sidestep_command | `0` | 0x020 |
|
||||||
|
| sidestep_holdkey | `HoldKey.Invalid` (0) | 0x040 |
|
||||||
|
| sidestep_speed | `1.0f` | 0x080 |
|
||||||
|
| turn_command | `0` | 0x100 |
|
||||||
|
| turn_holdkey | `HoldKey.Invalid` (0) | 0x200 |
|
||||||
|
| turn_speed | `1.0f` | 0x400 |
|
||||||
|
| actions (list) | empty | `num_actions<<11` (bits 11–15) |
|
||||||
|
|
||||||
|
Pure data, no GL/Net dependency. Lives in Core.Physics so the Phase-2 motion
|
||||||
|
interpreter (also Core.Physics) can populate it without a Core.Net dependency.
|
||||||
|
A `RawMotionState.Default` exposes the retail defaults.
|
||||||
|
|
||||||
|
**Packer** in `AcDream.Core.Net` (Core.Net may reference Core.Physics; the
|
||||||
|
reverse is forbidden by Code Structure Rule #2). Ports `RawMotionState::Pack`
|
||||||
|
verbatim:
|
||||||
|
1. Build flags dword: set bit *only* when the field **differs from its default**
|
||||||
|
(`forward_speed != 1.0f`, `current_holdkey != None`, etc.), OR in
|
||||||
|
`num_actions << 11`. **Note the bitfield is bits 0–15 only** — bits 16–31 are
|
||||||
|
unused (retail `num_actions` is 5 bits, max 31). The current
|
||||||
|
`MoveToState.cs` comment "bits 11–31 = command list length" is wrong; correct
|
||||||
|
it to bits 11–15 = `num_actions`.
|
||||||
|
2. Write the flags dword.
|
||||||
|
3. Write each set field in bit order (holdkey, style, fwd_cmd, fwd_holdkey,
|
||||||
|
fwd_speed, ss_cmd, ss_holdkey, ss_speed, turn_cmd, turn_holdkey, turn_speed).
|
||||||
|
4. Write each action: `u16 command` then
|
||||||
|
`u16 (stamp & 0x7FFF) | (autonomous ? 0x8000 : 0)` (decomp lines 293945–293960).
|
||||||
|
|
||||||
|
`MoveToState.Build` is refactored to take a `RawMotionState` (plus position,
|
||||||
|
sequences, contact, standingLongjump) instead of the flat nullable param list.
|
||||||
|
|
||||||
|
**Behavioral effect vs ACE:** the only runtime change is *removing* over-sent
|
||||||
|
defaults (`forwardSpeed=1.0`, `currentHoldKey=None`, default per-axis hold keys).
|
||||||
|
ACE's `UnPackNet` defaults omitted fields, so this is strictly more correct. The
|
||||||
|
slice does **not** change which RawMotionState values the caller constructs
|
||||||
|
(that is D6) — it threads the same values the caller passes today through the new
|
||||||
|
struct. Smoke-test against ACE confirms no regression.
|
||||||
|
|
||||||
|
### 3. MoveToState trailing byte (D3)
|
||||||
|
|
||||||
|
From `MoveToStatePack::Pack` (`0x005168f0`, lines 284717–284722): the trailing
|
||||||
|
byte is `((longjump_mode != 0 ? 0x02 : 0) | (contact != 0 ? 0x01 : 0))`. Then
|
||||||
|
ALIGN to 4.
|
||||||
|
|
||||||
|
`MoveToState.Build` gains explicit `bool contact` and `bool standingLongjump`
|
||||||
|
params. `standingLongjump` is wired to its true current value (`false` — standing
|
||||||
|
longjump is not implemented yet). The *packing* is faithful; the *input* is
|
||||||
|
honest. When standing longjump lands (later), the param flips. This is not a
|
||||||
|
masking shortcut — it is the correct byte with the correct current input.
|
||||||
|
|
||||||
|
### 4. JumpPack (D4)
|
||||||
|
|
||||||
|
From `JumpPack::Pack` (`0x00516d10`, lines 284934–284963), field order:
|
||||||
|
1. `f32 extent`
|
||||||
|
2. `f32 velocity.x, velocity.y, velocity.z`
|
||||||
|
3. `Position.Pack` (objcell_id + frame)
|
||||||
|
4. `u16 instance_timestamp, server_control_timestamp, teleport_timestamp, force_position_ts`
|
||||||
|
5. ALIGN to 4
|
||||||
|
|
||||||
|
`JumpAction.Build` is rewritten to this layout. The current spurious
|
||||||
|
`u32 objectGuid` + `u32 spellId` are **removed** (no retail evidence). New params:
|
||||||
|
`cellId`, `position`, `rotation`. Caller at `GameWindow.cs:8343` supplies them
|
||||||
|
(it already has `wireCellId` / `wirePos` / `wireRot` in scope).
|
||||||
|
|
||||||
|
### 5. AutonomousPosition (D5 audit + timestamp order)
|
||||||
|
|
||||||
|
`AutonomousPositionPack::Pack` (`0x00516af0`): Position, then `u16` instance /
|
||||||
|
server_control / teleport / force_position, then a **contact-only** byte
|
||||||
|
(`contact != 0`; no longjump bit), then ALIGN. Current code matches. Add:
|
||||||
|
- a timestamp-**order** golden test (instance, serverControl, teleport,
|
||||||
|
forcePosition);
|
||||||
|
- a contact-byte test.
|
||||||
|
|
||||||
|
**D5 audit (no behavior change unless decomp proves divergence):** read
|
||||||
|
`CommandInterpreter::SendMovementEvent` (`0x006b4680`) and `SendPositionEvent`
|
||||||
|
(`0x006b4770`) and compare what they stamp (`last_sent_position_time` vs
|
||||||
|
position vs contact_plane) to acdream's `NotePositionSent`. Document the finding;
|
||||||
|
change behavior only if the decomp shows a real divergence.
|
||||||
|
|
||||||
|
### 6. Position::Pack verification
|
||||||
|
|
||||||
|
`Position::Pack` is shared by all three messages and currently writes
|
||||||
|
`cellId, x, y, z, qw, qx, qy, qz`. It works against ACE, but for strict parity:
|
||||||
|
decompile `Position::Pack` / `Frame::Pack` by address, confirm the field order,
|
||||||
|
and lock it with a golden test on the Position block. Fix only if the decomp
|
||||||
|
diverges; otherwise the test pins it.
|
||||||
|
|
||||||
|
## Test inventory (written first, failing)
|
||||||
|
|
||||||
|
Core.Net.Tests:
|
||||||
|
- `RawMotionStatePackTests` — default state (only the flags dword, value `0`);
|
||||||
|
walk-forward speed 1.0 (forward_command + forward_holdkey set, **forward_speed
|
||||||
|
omitted**); run forward (holdkey + fwd_cmd + fwd_holdkey + fwd_speed); backward;
|
||||||
|
sidestep right/left; turn right/left; non-default current_style; a populated
|
||||||
|
actions list (num_actions bits + per-action u16 pair); contact/longjump
|
||||||
|
trailing byte combinations.
|
||||||
|
- `MoveToStateGoldenTests` — full-message golden bytes for walk, run, slow-walk
|
||||||
|
(toggle-run), sidestep, turn, contact, standing-longjump.
|
||||||
|
- `JumpActionTests` — retail `JumpPack` layout, full Position present, no
|
||||||
|
object/spell fields.
|
||||||
|
- `AutonomousPositionTests` — timestamp order + contact byte.
|
||||||
|
- `PositionPackTests` — Position block byte order.
|
||||||
|
|
||||||
|
Core.Tests:
|
||||||
|
- `MotionCommandCatalogTests` — ACE-mode + 2013-mode matrices; class-priority
|
||||||
|
collisions; the "override deleted" assertion (or per-command cited override).
|
||||||
|
- `MotionCommandCatalogDatTests` — real-DAT availability assertions.
|
||||||
|
|
||||||
|
Golden bytes are computed by hand from the decomp `Pack` functions (cited by
|
||||||
|
address in each test) and cross-checked against holtburger's Rust packers.
|
||||||
|
|
||||||
|
## Layering & placement
|
||||||
|
|
||||||
|
- `IMotionCommandCatalog`, `AceModernCommandCatalog`, `Retail2013CommandCatalog`,
|
||||||
|
`RawMotionState` → `src/AcDream.Core/Physics/`.
|
||||||
|
- `RawMotionState` packer + the three message builders → `src/AcDream.Core.Net/Messages/`.
|
||||||
|
- Caller wiring → `src/AcDream.App/Rendering/GameWindow.cs` (MoveToState ~8277,
|
||||||
|
Jump ~8343) — minimal: build a `RawMotionState`, pass new contact/longjump and
|
||||||
|
jump position params. No new feature body in `GameWindow` (Code Structure Rule #1).
|
||||||
|
- Tests in the project matching the layer (Rule #6).
|
||||||
|
|
||||||
|
## Deferred / unresolved (called out, not guessed)
|
||||||
|
|
||||||
|
- **D6 (next slice):** the runtime does not yet construct the exactly-retail
|
||||||
|
`RawMotionState` (run-vs-walk `forward_command`, hold-key derivation, backward
|
||||||
|
/ sidestep-left speed normalization). This slice makes the *packer* verbatim;
|
||||||
|
state construction is `adjust_motion`/`apply_raw_movement` (audit Phase 2).
|
||||||
|
- **D5:** heartbeat-clock stamping is *audited* here, changed only if proven.
|
||||||
|
- **D2:** actions-list / current_style packing is structurally supported and
|
||||||
|
tested, but the runtime emits 0 actions; populated emission is Phase 2+.
|
||||||
|
- **D9/D8/D10/D11/D12, D7:** later L.1/L.2 slices.
|
||||||
|
|
||||||
|
## Bookkeeping (mandatory)
|
||||||
|
|
||||||
|
- **Divergence register:** delete the rows for D1, D3, D4 in the same commit that
|
||||||
|
ports each. If any new deviation is introduced, add its row in the same commit.
|
||||||
|
- **Roadmap:** update L.2b (wire) and L.1b (catalog) "shipped" notes when landed.
|
||||||
|
- **Memory:** if a durable lesson emerges (e.g., the default-difference packing
|
||||||
|
pattern, the dual-catalog seam), capture it.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Failing oracle tests written first; then green after implementation.
|
||||||
|
- `dotnet build` + `dotnet test` green at each commit.
|
||||||
|
- Every ported algorithm cites the named retail symbol + address in test/comment.
|
||||||
|
- ACE smoke pass: login, walk/run/sidestep/turn/jump, observe no server
|
||||||
|
rejection; confirm the over-sent defaults are gone on the wire (dump or capture).
|
||||||
|
- Divergence register rows for D1/D3/D4 retired; roadmap updated.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue