acdream/docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md
Erik 33ad231d18 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>
2026-06-30 21:34:33 +02:00

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.