feat(L.2b): outbound movement wire parity — RawMotionState default-difference, JumpPack, contact byte
Ports the three retail outbound-movement packers verbatim (decomp-derived golden bytes, confirmed via the Ghidra bridge, cross-checked vs holtburger): - D1 — RawMotionState::Pack (0x0051ed10): new AcDream.Core.Physics.RawMotionState data type (11 fields + actions, retail defaults) + RawMotionStatePacker that sets a flag bit only when the field DIFFERS from its default. MoveToState.Build now takes a RawMotionState instead of presence-based nullable params, so the over-sent forwardSpeed=1.0 / currentHoldKey=None / default per-axis holdkeys are no longer emitted. num_actions packs into bits 11-15 (not "bits 11-31"). - D3 — MoveToStatePack::Pack (0x005168f0) trailing byte = (standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0); explicit contact/ standingLongjump params (standingLongjump=false honestly until the feature lands). - D4 — JumpAction rewritten to retail JumpPack::Pack (0x00516d10): extent, velocity, full Position, four u16 timestamps, align. Removed the spurious objectGuid/spellId u32s; Position is now packed (it was absent). Body 56 bytes. - Position::Pack (0x005a9640) / Frame::Pack (0x00535130) verified already-correct (cellId, origin xyz, quaternion wxyz); locked with a golden test, no change. GameWindow callers adapted minimally: build the RawMotionState from the existing MovementResult values (behavior preserved except the intended D1 omissions) and pass cellId/position/rotation to the Jump send. Pre-existing MotionInterpreter placeholder struct RawMotionState renamed LegacyRawMotionState (D6/Phase-2 scope, pure rename) to free the name for the retail-faithful type. D5 audit: confirmed a real divergence — retail SendMovementEvent (0x006b4680) stamps only last_sent_position_time after an MTS while SendPositionEvent (0x006b4770) stamps all three; acdream's NotePositionSent stamps all three on both paths. Left unchanged (comments added at both call sites), recorded as register TS-33, deferred to a dedicated cadence-port slice. Tests: RawMotionStatePackTests / MoveToStateGoldenTests / JumpActionTests / PositionPackTests + updated MoveToStateTests / AutonomousPositionTests. Full suite green (Core.Net.Tests 372, full solution 3228 passed / 4 pre-existing skips). Register: TS-24/TS-25 refreshed (packer now supports actions/style; runtime emission still deferred), TS-33 added. Roadmap L.2b shipped note added. Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md (2-6) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2c8620ea94
commit
78e163a41e
15 changed files with 1173 additions and 212 deletions
|
|
@ -175,7 +175,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 4. Temporary stopgap (TS) — 32 rows
|
||||
## 4. Temporary stopgap (TS) — 33 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -202,8 +202,8 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-21 | Default run/jump skills 200/300 tuned to feel until the first PlayerDescription lands; "we don't parse yet" comment is STALE (K-fix7 parses PD → SetCharacterSkills) | `src/AcDream.App/Input/PlayerMovementController.cs:341` | Defaults rule only pre-PD or on PD parse failure; jump bumped 200→300 on user complaint (3.01 m max felt too low) | Any window with defaults live predicts run/jump speeds the server disagrees with — observer rubber-banding, local snap-backs | retail height = (skill/(skill+1300))×22.2 + 0.05 |
|
||||
| TS-22 | `adjust_motion` not ported — backward (×−0.65) / strafe (×−1) translation hand-mirrored at controller call sites; `get_state_velocity` returns (0,0,0) for backward/strafe-left | `src/AcDream.App/Input/PlayerMovementController.cs:1021` | Duplication exists because LeaveGround through the unported path wiped strafe/backward jump velocity (straight-up backward jumps) | Any NEW `get_state_velocity` consumer during backward/strafe motion silently gets zero velocity (the exact prior bug class); hand-mirrored formulas can drift from the grounded block they copy | FUN_00528010 (adjust_motion); FUN_00528960 |
|
||||
| TS-23 | PK/PKLite/Impenetrable mover bits never set (PlayerKillerStatus not parsed from PD); moverFlags always `IsPlayer ∣ EdgeSlide` | `src/AcDream.App/Input/PlayerMovementController.cs:1128` | Non-PK pair walks through other non-PK players — retail's default for ACE's character-creation defaults too | On a PK/PKLite character, local client lets players walk through where retail collides — prediction vs server disagree the moment PvP statuses enter play | PWD._bitfield acclient.h:6431-6463; pc:406898-406918 |
|
||||
| TS-24 | RawMotionState command list always empty (bits 11-31 = 0) — discrete motion events (emotes, one-shots) never packed outbound | `src/AcDream.Core.Net/Messages/MoveToState.cs:34` | Discrete client-initiated motions aren't implemented yet; documented builder scope | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | RawMotionState pack (holtburger types.rs) |
|
||||
| TS-25 | `FlagCurrentStyle` (stance, bit 0x2) never written to outbound MoveToState | `src/AcDream.Core.Net/Messages/MoveToState.cs:130` | Stance switching is M2 combat scope | Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement | RawMotionFlags CurrentStyle 0x2 (holtburger) |
|
||||
| TS-24 | RawMotionState action list always empty at runtime — the packer now emits `num_actions` (bits 11–15) + per-action u16 pairs (L.2b, `RawMotionState::Pack` 0x0051ed10), but the outbound caller builds an empty `Actions` list, so discrete motion events (emotes, one-shots) are still never broadcast | `src/AcDream.App/Rendering/GameWindow.cs:8297` (empty Actions); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:91` | Discrete client-initiated motions (D2) not wired yet; packer-ready, runtime emission is Phase 2+ | When player-triggered emotes land, they silently never broadcast — observers see idle while the local client animates | `RawMotionState::Pack` 0x0051ed10; num_actions `PackBitfield` acclient.h:46487 |
|
||||
| TS-25 | `current_style` (stance, flag bit 0x2) never populated at runtime — the packer now emits it when it differs from the retail default 0x8000003D (L.2b), but the outbound caller leaves `CurrentStyle` at default (stance not tracked here) | `src/AcDream.App/Rendering/GameWindow.cs:8286` (CurrentStyle left default); packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs:80` | Stance switching is M2 combat scope | Once combat-mode switching ships, mid-stance MoveToStates omit the style — server/observers keep the stale stance, wrong cycle family for every subsequent movement | `RawMotionState::Pack` current_style 0x0051ed10 |
|
||||
| TS-26 | UpdatePosition's four u16 sequence numbers parsed but never checked for freshness; retail rejects stale/out-of-order packets | `src/AcDream.Core.Net/Messages/UpdatePosition.cs:30` | Loopback ACE rarely reorders, so the gap is invisible in the dev loop | On a real network, a reordered/post-teleport straggler applies as-is — remotes snap backward / flicker; a teleport-vs-position race renders an entity in the wrong cell | PositionPack trailer (ACE PositionPack.cs::Write) |
|
||||
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
||||
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
|
||||
|
|
@ -211,6 +211,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-30 | Numbered chat tabs (element ids `0x10000522`–`0x10000525`) render as clickable buttons but do not switch channel filter or affect the transcript — tab state is a no-op | `src/AcDream.App/UI/Layout/ChatWindowController.cs:210` | Retail's tab switching routes transcript lines by chat channel (`gmMainChatUI::gmScrollWindow` sub-windows per tab); the tab wiring is D.5 scope | Tab clicks produce no visible transcript change; retail would filter to the selected channel — all chat always shows in all tabs | `gmMainChatUI::PostInit` tab setup @0x4ce2a0; holtburger chat tab handling |
|
||||
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
||||
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
||||
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping) is a dedicated follow-up slice | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -612,6 +612,11 @@ change must keep both phase plans in sync.
|
|||
- **L.1b — Command router + motion-state cleanup.** Extract tested
|
||||
`SetCycle` vs `PlayAction` routing, add missing `MotionCommand` constants,
|
||||
and split death `Sanctuary` action from persistent `Dead` substate.
|
||||
*(Partial, shipped 2026-06-30:* dual `IMotionCommandCatalog` seam —
|
||||
`AceModernCommandCatalog` runtime default + full-extraction
|
||||
`Retail2013CommandCatalog` conformance; deleted the blind 0x016E–0x0197
|
||||
override; exposed + filed the `CombatAnimationPlanner` 2013-numbering bug
|
||||
as ISSUES #159. Spec: `docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md`.)
|
||||
- **L.1c — Combat animation wiring.** Combat mode tracking, draw/sheath
|
||||
style transitions, attack swings by stance/power/height, hit reactions,
|
||||
evades/blocks/parries, and death handoff.
|
||||
|
|
@ -673,6 +678,20 @@ diagnostic scaffolding, not yet the final collision system.
|
|||
observer view. ACE accepting a position is a compatibility check, not proof
|
||||
of fine-grained retail collision parity.
|
||||
|
||||
**Shipped (2026-06-30) — L.2b outbound wire parity (decomp-verbatim, tests-first):**
|
||||
- **D1** — `MoveToState` now packs `RawMotionState` by retail default-difference
|
||||
(`RawMotionState::Pack` 0x0051ed10): a field bit is set only when the value
|
||||
differs from its retail default, so over-sent `forwardSpeed=1.0` / `holdKey=None`
|
||||
are gone. New `AcDream.Core.Physics.RawMotionState` data type + `RawMotionStatePacker`.
|
||||
- **D3** — MoveToState trailing byte = `(standingLongjump?2:0)|(contact?1:0)`
|
||||
(`MoveToStatePack::Pack` 0x005168f0); explicit `contact`/`standingLongjump` params.
|
||||
- **D4** — `JumpAction` rewritten to retail `JumpPack` (0x00516d10): extent · velocity ·
|
||||
full Position · 4 timestamps; spurious objectGuid/spellId removed.
|
||||
- **D5** — audited + confirmed divergent (MTS over-stamping vs retail `SendMovementEvent`);
|
||||
left unchanged, recorded as register **TS-33**, deferred to a dedicated cadence-port slice.
|
||||
- Golden-byte tests (Core.Net.Tests) for all packers; full suite green.
|
||||
- Spec: `docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md`.
|
||||
|
||||
**Acceptance:**
|
||||
- A developer can trace the active movement path: input/motion -> body
|
||||
prediction -> `ResolveWithTransition` -> contact/cell result -> outbound
|
||||
|
|
|
|||
|
|
@ -261,7 +261,15 @@ address in each test) and cross-checked against holtburger's Rust packers.
|
|||
`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.
|
||||
- **D5 (CONFIRMED 2026-06-30, deferred):** the audit found a real divergence —
|
||||
retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time`
|
||||
after an MTS, while `SendPositionEvent` (0x006b4770, AP) stamps all three
|
||||
(time + position + contact-plane); acdream's `NotePositionSent` stamps all three
|
||||
after both. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where
|
||||
retail uses `ShouldSendPositionEvent` (0x006b45e0). Left unchanged this slice (code
|
||||
comments added at both call sites); recorded as register row **TS-33** and deferred
|
||||
to a dedicated cadence-port follow-up slice (port `ShouldSendPositionEvent` + split
|
||||
MTS/AP stamping).
|
||||
- **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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue