feat(R4-V3): wire completion - mt 8/9 parsing + full params exposure + the mt-0 sticky trailer (closes M7, M13, M14-wire-note)
UpdateMotion now parses TurnToObject (mt 8: guid, standalone wire heading, 3-dword UnPackNet) and TurnToHeading (mt 9: 3-dword UnPackNet) into a new TurnToPathData sibling record (the two wire forms genuinely diverge - 7-dword move UnPackNet with Origin head vs 3-dword turn UnPackNet with guid+heading head; every consumer switches on MovementType first, so no polymorphic shape was invented). mt 6/7 exposure widened additively so ALL UnPackNet fields reach MovementParameters.FromWire. The mt=0 motionFlags sticky-guid trailer (bit 0x1) is parsed for cursor honesty and carried unconsumed until R5 - scoped to mt=0 ONLY per both ACE's writer (MovementInvalid.Write) and the decomp's case-0 read, tighter than the plan sketched; the StandingLongJump bit (0x2) doc-noted as the R5 unpack_movement item. MoveToRunRate doc-pointered as the V4/V5 MyRunRate write. 11 new golden-byte tests hand-assembled from ACE's writers (MovementData/TurnToObject/TurnToParameters/TurnToHeading/ MoveToParameters/MovementInvalid) incl. flag-permutation round-trips and the trailer cursor-honesty case; the existing 12 mt 6/7 fixtures pass unchanged. Full suite: 3,972 passed. Implemented by a dedicated agent against the V0-pinned spec (P6 order confirmed exactly); scope + suite independently verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
addc8e97a8
commit
a144e87318
3 changed files with 542 additions and 5 deletions
|
|
@ -34,7 +34,20 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// u32 flagsAndCommandCount, then each present field in flag order
|
||||
/// (CurrentStyle u16, ForwardCommand u16, SidestepCommand u16,
|
||||
/// TurnCommand u16, forward speed f32, sidestep speed f32,
|
||||
/// turn speed f32), commands list, align.</item>
|
||||
/// turn speed f32), commands list, align; THEN — only when
|
||||
/// <c>motionFlags & 0x1</c> (StickToObject) — one trailing u32
|
||||
/// sticky object guid (ACE <c>MovementInvalid.Write</c>; R4-V3).</item>
|
||||
/// <item>MoveToObject (6) / MoveToPosition (7): [u32 target guid,
|
||||
/// MoveToObject only] Origin (u32 cell + 3×f32), MoveToParameters /
|
||||
/// <c>UnPackNet</c> 7-dword form (u32 bitfield + 6×f32), f32
|
||||
/// runRate. R4-V3: exposed via
|
||||
/// <see cref="AcDream.Core.Net.Messages.CreateObject.MoveToPathData"/>.</item>
|
||||
/// <item>TurnToObject (8) / TurnToHeading (9): [u32 target guid, f32
|
||||
/// standalone wire heading, TurnToObject only] TurnToParameters /
|
||||
/// <c>UnPackNet</c> 3-dword form (u32 bitfield, f32 speed, f32
|
||||
/// desired heading). R4-V3 (closes M7): exposed via
|
||||
/// <see cref="AcDream.Core.Net.Messages.CreateObject.TurnToPathData"/>
|
||||
/// — previously dropped end-to-end.</item>
|
||||
/// </list>
|
||||
/// </item>
|
||||
/// </list>
|
||||
|
|
@ -132,7 +145,20 @@ public static class UpdateMotion
|
|||
// movementType u8, motionFlags u8, currentStyle u16
|
||||
if (body.Length - pos < 4) return null;
|
||||
byte movementType = body[pos]; pos += 1;
|
||||
byte _motionFlags = body[pos]; pos += 1;
|
||||
// MotionFlags (ACE ACE.Entity.Enum.MotionFlags, byte):
|
||||
// 0x1 = StickToObject, 0x2 = StandingLongJump.
|
||||
// R4-V3 (closes M14-wire-note): previously discarded as
|
||||
// `_motionFlags`. StickToObject now drives the sticky-guid
|
||||
// trailer parse below (mt=0 only, per ACE MovementInvalid.Write
|
||||
// + decomp §2f case 0 @0052455d). StandingLongJump (0x2) is
|
||||
// DOCUMENTED here but NOT consumed — retail's
|
||||
// `unpack_movement` case 0 writes it onto
|
||||
// `motion_interpreter->standing_longjump` (§2f @0052458e), an
|
||||
// R5 unpack_movement item (this parser has no MotionInterpreter
|
||||
// reference to write it onto; the bit is simply not read past
|
||||
// this comment, matching "not consumed" rather than
|
||||
// mis-consumed).
|
||||
byte motionFlags = body[pos]; pos += 1;
|
||||
ushort currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
|
||||
|
|
@ -142,7 +168,7 @@ public static class UpdateMotion
|
|||
var hex = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < preHex; i++) hex.Append($"{body[i]:X2} ");
|
||||
System.Console.WriteLine(
|
||||
$" UM raw: mt=0x{movementType:X2} mf=0x{_motionFlags:X2} cs=0x{currentStyle:X4} | {hex}");
|
||||
$" UM raw: mt=0x{movementType:X2} mf=0x{motionFlags:X2} cs=0x{currentStyle:X4} | {hex}");
|
||||
}
|
||||
|
||||
ushort? forwardCommand = null;
|
||||
|
|
@ -155,6 +181,8 @@ public static class UpdateMotion
|
|||
float? moveToSpeed = null;
|
||||
float? moveToRunRate = null;
|
||||
CreateObject.MoveToPathData? moveToPath = null;
|
||||
CreateObject.TurnToPathData? turnToPath = null;
|
||||
uint? stickyObjectGuid = null;
|
||||
List<CreateObject.MotionItem>? commands = null;
|
||||
|
||||
if (movementType == 0)
|
||||
|
|
@ -247,6 +275,23 @@ public static class UpdateMotion
|
|||
commands.Add(new CreateObject.MotionItem(cmd, seq, speed));
|
||||
}
|
||||
}
|
||||
|
||||
// R4-V3 (closes M14-wire-note): the sticky-guid trailer.
|
||||
// ACE MovementInvalid.Write (Motion/MovementInvalid.cs:41-47)
|
||||
// writes the InterpretedMotionState FIRST (everything above,
|
||||
// including the Commands list), THEN — only when
|
||||
// MotionFlags.StickToObject (0x1) is set — one trailing u32
|
||||
// guid. Decomp confirms the same order (§2f case 0:
|
||||
// InterpretedMotionState::UnPack, then
|
||||
// `if (header & 0x100) sticky_object_guid = read_dword()`).
|
||||
// Parsed here purely to keep the cursor honest past this
|
||||
// field; R5's PositionManager::StickTo body is the eventual
|
||||
// consumer (M14 — "seam now, body R5").
|
||||
if ((motionFlags & 0x1) != 0 && body.Length - pos >= 4)
|
||||
{
|
||||
stickyObjectGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
done:;
|
||||
}
|
||||
else if (movementType is 6 or 7)
|
||||
|
|
@ -260,6 +305,14 @@ public static class UpdateMotion
|
|||
out moveToRunRate,
|
||||
out moveToPath);
|
||||
}
|
||||
else if (movementType is 8 or 9)
|
||||
{
|
||||
TryParseTurnToPayload(
|
||||
body,
|
||||
pos,
|
||||
movementType,
|
||||
out turnToPath);
|
||||
}
|
||||
|
||||
return new Parsed(guid, new CreateObject.ServerMotionState(
|
||||
currentStyle, forwardCommand, forwardSpeed, commands,
|
||||
|
|
@ -268,7 +321,9 @@ public static class UpdateMotion
|
|||
moveToParameters,
|
||||
moveToSpeed,
|
||||
moveToRunRate,
|
||||
moveToPath),
|
||||
moveToPath,
|
||||
turnToPath,
|
||||
stickyObjectGuid),
|
||||
instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
}
|
||||
catch
|
||||
|
|
@ -354,4 +409,70 @@ public static class UpdateMotion
|
|||
desiredHeading);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-V3 (closes M7) — parses movementType 8 (TurnToObject) / 9
|
||||
/// (TurnToHeading), byte-for-byte per V0-pins.md P6 (built + confirmed
|
||||
/// against ACE's own writers: <c>TurnToObjectExtensions.Write</c>,
|
||||
/// <c>TurnToParametersExtensions.Write</c>,
|
||||
/// <c>TurnToHeadingExtensions.Write</c> —
|
||||
/// <c>references/ACE/Source/ACE.Server/Network/Motion/</c>):
|
||||
/// <list type="bullet">
|
||||
/// <item>type 8 (TurnToObject) only: u32 <c>object guid</c>, f32
|
||||
/// <c>wire_heading</c> (the STANDALONE heading field — ACE
|
||||
/// <c>TurnToObject.DesiredHeading</c>, "used instead of the
|
||||
/// DesiredHeading in the TurnToParameters" per its own field
|
||||
/// comment).</item>
|
||||
/// <item>TurnToParameters / <c>MovementParameters::UnPackNet</c>
|
||||
/// 3-dword TurnTo form (0xc bytes, decomp §2g), present for BOTH
|
||||
/// types: u32 <c>bitfield</c>, f32 <c>speed</c>, f32
|
||||
/// <c>desired_heading</c>.</item>
|
||||
/// </list>
|
||||
/// Matches retail's read order exactly (decomp §2f case 8:
|
||||
/// <c>object_id = read_dword(); wire_heading = read_dword();
|
||||
/// UnPackNet(...)</c>). The unresolvable-object fallback (substitute
|
||||
/// <c>wire_heading</c> into <c>params.desired_heading</c>, degrade to
|
||||
/// TurnToHeading) is a CONSUMER decision (needs a live object-id
|
||||
/// resolution the wire parser has no visibility into) — this method
|
||||
/// only exposes both heading fields distinctly so that fallback can be
|
||||
/// implemented at the call site (V4/V5, GameWindow routing per
|
||||
/// r4-port-plan.md §4).
|
||||
/// </summary>
|
||||
private static bool TryParseTurnToPayload(
|
||||
ReadOnlySpan<byte> body,
|
||||
int pos,
|
||||
byte movementType,
|
||||
out CreateObject.TurnToPathData? path)
|
||||
{
|
||||
path = null;
|
||||
|
||||
uint? targetGuid = null;
|
||||
float? wireHeading = null;
|
||||
if (movementType == 8)
|
||||
{
|
||||
if (body.Length - pos < 8) return false;
|
||||
targetGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
wireHeading = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// TurnToParameters / UnPackNet 3-dword TurnTo form (0xc bytes):
|
||||
// bitfield, speed, desired_heading.
|
||||
if (body.Length - pos < 12) return false;
|
||||
|
||||
uint bitfield = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float speed = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float desiredHeading = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
|
||||
path = new CreateObject.TurnToPathData(
|
||||
targetGuid,
|
||||
wireHeading,
|
||||
bitfield,
|
||||
speed,
|
||||
desiredHeading);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue