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
|
|
@ -8263,29 +8263,45 @@ public sealed class GameWindow : IDisposable
|
|||
// semantic.
|
||||
if (result.MotionStateChanged && !_playerController.IsServerAutoWalking)
|
||||
{
|
||||
// HoldKey axis values — retail enum (holtburger types.rs HoldKey):
|
||||
// Invalid = 0, None = 1, Run = 2
|
||||
// HoldKey axis values — retail enum (acclient.h enum
|
||||
// HoldKey): Invalid = 0, None = 1, Run = 2.
|
||||
// Retail always sends CURRENT_HOLD_KEY (and uses the same
|
||||
// value for every active per-axis hold key — see
|
||||
// holtburger's build_motion_state_raw_motion_state).
|
||||
// When the player is running forward, 2=Run; otherwise 1=None.
|
||||
const uint HoldKeyNone = 1u;
|
||||
const uint HoldKeyRun = 2u;
|
||||
uint axisHoldKey = result.IsRunning ? HoldKeyRun : HoldKeyNone;
|
||||
// When the player is running forward, Run; otherwise None.
|
||||
var axisHoldKey = result.IsRunning
|
||||
? AcDream.Core.Physics.HoldKey.Run
|
||||
: AcDream.Core.Physics.HoldKey.None;
|
||||
|
||||
// D1/L.2b (2026-06-30): build the COMPLETE RawMotionState
|
||||
// snapshot (matching retail's CPhysicsObj::InqRawMotionState())
|
||||
// and let RawMotionStatePacker's default-difference comparison
|
||||
// decide what actually goes on the wire — see
|
||||
// RawMotionStatePacker (ports RawMotionState::Pack, 0x0051ed10).
|
||||
// This slice changes ONLY the packing; the state values
|
||||
// below are exactly what the pre-slice code passed in.
|
||||
var rawMotionState = new AcDream.Core.Physics.RawMotionState
|
||||
{
|
||||
CurrentHoldKey = axisHoldKey,
|
||||
// CurrentStyle: not tracked by acdream today — leave at
|
||||
// the retail default (0x8000003D) so the packer omits it.
|
||||
ForwardCommand = result.ForwardCommand ?? AcDream.Core.Physics.RawMotionState.Default.ForwardCommand,
|
||||
ForwardHoldKey = result.ForwardCommand.HasValue ? axisHoldKey : AcDream.Core.Physics.HoldKey.Invalid,
|
||||
ForwardSpeed = result.ForwardSpeed ?? AcDream.Core.Physics.RawMotionState.Default.ForwardSpeed,
|
||||
SidestepCommand = result.SidestepCommand ?? AcDream.Core.Physics.RawMotionState.Default.SidestepCommand,
|
||||
SidestepHoldKey = result.SidestepCommand.HasValue ? axisHoldKey : AcDream.Core.Physics.HoldKey.Invalid,
|
||||
SidestepSpeed = result.SidestepSpeed ?? AcDream.Core.Physics.RawMotionState.Default.SidestepSpeed,
|
||||
TurnCommand = result.TurnCommand ?? AcDream.Core.Physics.RawMotionState.Default.TurnCommand,
|
||||
TurnHoldKey = result.TurnCommand.HasValue ? axisHoldKey : AcDream.Core.Physics.HoldKey.Invalid,
|
||||
TurnSpeed = result.TurnSpeed ?? AcDream.Core.Physics.RawMotionState.Default.TurnSpeed,
|
||||
// Actions: acdream does not yet emit discrete motion
|
||||
// events on this path (D2, deferred to Phase 2+).
|
||||
};
|
||||
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.MoveToState.Build(
|
||||
gameActionSequence: seq,
|
||||
forwardCommand: result.ForwardCommand,
|
||||
forwardSpeed: result.ForwardSpeed,
|
||||
sidestepCommand: result.SidestepCommand,
|
||||
sidestepSpeed: result.SidestepSpeed,
|
||||
turnCommand: result.TurnCommand,
|
||||
turnSpeed: result.TurnSpeed,
|
||||
holdKey: axisHoldKey, // always present
|
||||
forwardHoldKey: result.ForwardCommand.HasValue ? axisHoldKey : (uint?)null,
|
||||
sidestepHoldKey: result.SidestepCommand.HasValue ? axisHoldKey : (uint?)null,
|
||||
turnHoldKey: result.TurnCommand.HasValue ? axisHoldKey : (uint?)null,
|
||||
rawMotionState: rawMotionState,
|
||||
cellId: wireCellId,
|
||||
position: wirePos,
|
||||
rotation: wireRot,
|
||||
|
|
@ -8293,15 +8309,25 @@ public sealed class GameWindow : IDisposable
|
|||
serverControlSequence: _liveSession.ServerControlSequence,
|
||||
teleportSequence: _liveSession.TeleportSequence,
|
||||
forcePositionSequence: _liveSession.ForcePositionSequence,
|
||||
contactLongJump: contactByte);
|
||||
contact: contactByte != 0,
|
||||
// Standing longjump is not implemented yet — honest
|
||||
// current value, not a guess (D3).
|
||||
standingLongjump: false);
|
||||
DumpMovementTruthOutbound(
|
||||
"MTS", seq, result, wirePos, wireCellId, contactByte);
|
||||
_liveSession.SendGameAction(body);
|
||||
// B.6/B.7 (2026-05-16): stamp the diff-driven heartbeat clock so
|
||||
// HeartbeatDue resets its interval from THIS send — mirrors retail's
|
||||
// SendMovementEvent (acclient_2013_pseudo_c.txt:0x006b4680) writing
|
||||
// last_sent_position_time + last_sent_position + contact_plane
|
||||
// after each MTS send.
|
||||
// D5 audit (2026-06-30, this slice): retail's
|
||||
// SendMovementEvent (acclient_2013_pseudo_c.txt, 0x006b4680)
|
||||
// stamps ONLY last_sent_position_time after an MTS send — it
|
||||
// does NOT update last_sent_position or
|
||||
// last_sent_contact_plane (confirmed via Ghidra
|
||||
// decompile-by-address during this slice). Only
|
||||
// SendPositionEvent (the AP path, below) stamps all three.
|
||||
// acdream's NotePositionSent call here stamps all three on
|
||||
// BOTH paths — this is a real, audit-confirmed divergence
|
||||
// from retail. Per this slice's scope (D5 = audit only,
|
||||
// change only with explicit sign-off), left UNCHANGED;
|
||||
// reported to the lead engineer instead of fixed here.
|
||||
_playerController.NotePositionSent(
|
||||
worldPos: _playerController.Position,
|
||||
cellId: _playerController.CellId,
|
||||
|
|
@ -8327,9 +8353,13 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSession.SendGameAction(body);
|
||||
// B.6/B.7 (2026-05-16): stamp the diff-driven heartbeat clock so
|
||||
// HeartbeatDue resets its interval from THIS send — mirrors retail's
|
||||
// SendPositionEvent (acclient_2013_pseudo_c.txt:700345-700348)
|
||||
// SendPositionEvent (acclient_2013_pseudo_c.txt:700345-700348,
|
||||
// decomp address 0x006b4770, re-confirmed via the Ghidra
|
||||
// decompile-by-address bridge during the L.2b slice 2026-06-30)
|
||||
// writing last_sent_position_time + last_sent_position +
|
||||
// last_sent_contact_plane after each AP.
|
||||
// last_sent_contact_plane after each AP. Unlike the MTS path
|
||||
// above (SendMovementEvent, 0x006b4680), which retail stamps
|
||||
// ONLY last_sent_position_time, this AP path is correct.
|
||||
_playerController.NotePositionSent(
|
||||
worldPos: _playerController.Position,
|
||||
cellId: _playerController.CellId,
|
||||
|
|
@ -8339,11 +8369,17 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
if (result.JumpExtent.HasValue && result.JumpVelocity.HasValue)
|
||||
{
|
||||
// D4/L.2b (2026-06-30): JumpPack::Pack (0x00516d10) packs the
|
||||
// full Position, not an objectGuid/spellId — pass the same
|
||||
// wireCellId/wirePos/wireRot the MoveToState send above uses.
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var jumpBody = AcDream.Core.Net.Messages.JumpAction.Build(
|
||||
gameActionSequence: seq,
|
||||
extent: result.JumpExtent.Value,
|
||||
velocity: result.JumpVelocity.Value,
|
||||
cellId: wireCellId,
|
||||
position: wirePos,
|
||||
rotation: wireRot,
|
||||
instanceSequence: _liveSession.InstanceSequence,
|
||||
serverControlSequence: _liveSession.ServerControlSequence,
|
||||
teleportSequence: _liveSession.TeleportSequence,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue