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>
151 lines
6.4 KiB
Markdown
151 lines
6.4 KiB
Markdown
# 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.
|