Merge branch 'main' into claude/peaceful-visvesvaraya-e0a196
# Conflicts: # docs/ISSUES.md # docs/architecture/retail-divergence-register.md
This commit is contained in:
commit
217a4bad69
329 changed files with 81439 additions and 8499 deletions
|
|
@ -119,6 +119,9 @@ public static class CreateObject
|
|||
ushort TeleportSequence = 0,
|
||||
ushort ServerControlSequence = 0,
|
||||
ushort ForcePositionSequence = 0,
|
||||
// L.2g S1 (DEV-6): ObjectMovement stamp (timestamp block index 1)
|
||||
// seeds MotionSequenceGate's MOVEMENT_TS at spawn.
|
||||
ushort MovementSequence = 0,
|
||||
uint? PhysicsState = null,
|
||||
uint? ObjectDescriptionFlags = null,
|
||||
// L.3b (2026-04-30): per-object friction + elasticity from the
|
||||
|
|
@ -215,6 +218,21 @@ public static class CreateObject
|
|||
/// 0x40=TurnSpeed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="MoveToRunRate">
|
||||
/// R4-V3 deliverable D — the trailing <c>f32 runRate</c> on MoveToObject
|
||||
/// (6) / MoveToPosition (7) payloads. Retail's <c>unpack_movement</c>
|
||||
/// writes this straight onto <c>CMotionInterp::my_run_rate</c>
|
||||
/// (r4-moveto-decomp.md §2f: <c>this->motion_interpreter->my_run_rate =
|
||||
/// read_float()</c>, both @300603 case 6 and @300660 case 7 — SAME
|
||||
/// write for both types, immediately after <c>UnPackNet</c>). Today this
|
||||
/// field only seeds <c>PlanMoveToStart</c>'s local heuristic (plan M13);
|
||||
/// the interp's actual <see cref="AcDream.Core.Physics.MotionInterpreter.MyRunRate"/>
|
||||
/// field is a SEPARATE consumer write the V4/V5 MoveToManager cutover
|
||||
/// performs at the GameWindow mt 6-9 routing site (r4-port-plan.md §4,
|
||||
/// step 2: <c>Motion.MyRunRate = MoveToRunRate</c>) — this record is
|
||||
/// wire-primitive only; it does not write MotionInterpreter state
|
||||
/// itself.
|
||||
/// </param>
|
||||
public readonly record struct ServerMotionState(
|
||||
ushort Stance,
|
||||
ushort? ForwardCommand,
|
||||
|
|
@ -228,7 +246,30 @@ public static class CreateObject
|
|||
uint? MoveToParameters = null,
|
||||
float? MoveToSpeed = null,
|
||||
float? MoveToRunRate = null,
|
||||
MoveToPathData? MoveToPath = null)
|
||||
MoveToPathData? MoveToPath = null,
|
||||
// R4-V3 (closes M7): movement types 8 (TurnToObject) and 9
|
||||
// (TurnToHeading) — previously dropped end-to-end (UpdateMotion.cs
|
||||
// only branched on `movementType is 6 or 7`). Carries the DECODED
|
||||
// wire payload (guid + standalone wire_heading for type 8, plus the
|
||||
// shared 3-dword UnPackNet triple for both types) per V0-pins.md P6.
|
||||
TurnToPathData? TurnToPath = null,
|
||||
// R4-V3 (closes M14-wire-note): the 0xF74C motionFlags sticky-guid
|
||||
// trailer, mt=0 (Invalid) only — ACE MovementInvalid.Write gates the
|
||||
// trailing guid on MotionFlags.StickToObject (0x1); the decomp's
|
||||
// `unpack_movement` case 0 reads it right after
|
||||
// InterpretedMotionState::UnPack (r4-moveto-decomp.md §2f
|
||||
// @0052455d: `if (header & 0x100) sticky_object_guid = read_dword()`
|
||||
// — bit 0x100 of the combined header word is motionFlags byte1&0x1).
|
||||
// R5-V4 consumes it: the GameWindow mt-0 tail routes it into
|
||||
// CPhysicsObj::stick_to_object's port (target PartArray radii →
|
||||
// PositionManager.StickTo — decomp 0x005127e0, call @00524589).
|
||||
uint? StickyObjectGuid = null,
|
||||
// R5-V4 (closes the "documented but NOT consumed" note in
|
||||
// UpdateMotion.cs): motionFlags & 0x2 — retail `unpack_movement`
|
||||
// case 0 writes it onto `motion_interpreter->standing_longjump`
|
||||
// UNCONDITIONALLY (@0052458e: absent flag CLEARS it). Consumed at
|
||||
// the GameWindow mt-0 tails (remote + player).
|
||||
bool StandingLongJump = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE/retail movement types 6 and 7 are server-controlled
|
||||
|
|
@ -238,6 +279,13 @@ public static class CreateObject
|
|||
/// </summary>
|
||||
public bool IsServerControlledMoveTo => MovementType is 6 or 7;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V3: movement types 8 (TurnToObject) and 9 (TurnToHeading) —
|
||||
/// the turn-only sibling of <see cref="IsServerControlledMoveTo"/>.
|
||||
/// Neither carries an InterpretedMotionState.ForwardCommand either.
|
||||
/// </summary>
|
||||
public bool IsServerControlledTurnTo => MovementType is 8 or 9;
|
||||
|
||||
public bool MoveToCanRun => !MoveToParameters.HasValue
|
||||
|| (MoveToParameters.Value & 0x2u) != 0;
|
||||
|
||||
|
|
@ -297,6 +345,51 @@ public static class CreateObject
|
|||
float MinDistance,
|
||||
float FailDistance,
|
||||
float WalkRunThreshold,
|
||||
float DesiredHeading,
|
||||
uint Bitfield = 0); // R4-V4: the raw UnPackNet flags dword, feeds MovementParameters.FromWire
|
||||
|
||||
/// <summary>
|
||||
/// R4-V3 (closes M7) — path-control payload of a server-controlled
|
||||
/// TurnTo packet (movementType 8 TurnToObject or 9 TurnToHeading).
|
||||
/// Sibling of <see cref="MoveToPathData"/>: kept as a SEPARATE record
|
||||
/// rather than widening <c>MoveToPathData</c> in place, because the two
|
||||
/// wire forms genuinely diverge (7-dword <c>UnPackNet</c> with an
|
||||
/// Origin+optional-guid head for move types vs. the 3-dword
|
||||
/// <c>UnPackNet</c> with a guid+standalone-heading head for turn types —
|
||||
/// V0-pins.md P6) and a single record would need every move-only field
|
||||
/// nullable for turn payloads (and vice versa) for no reader benefit —
|
||||
/// no code path ever needs "either a move or a turn path" polymorphically,
|
||||
/// every consumer already switches on <see cref="ServerMotionState.MovementType"/>
|
||||
/// first.
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>type 8 (TurnToObject) only: u32 <c>TargetGuid</c>, f32
|
||||
/// <c>WireHeading</c> — the STANDALONE heading field (ACE
|
||||
/// <c>TurnToObject.DesiredHeading</c>, distinct from
|
||||
/// <see cref="DesiredHeading"/> below despite ACE always populating
|
||||
/// both from the same source; V0-pins.md P6's fixture caveat: never
|
||||
/// distinguish the two fields by value in a test, only by
|
||||
/// OFFSET). Consumed ONLY in retail's unresolvable-object fallback
|
||||
/// (decomp §2f case 8: <c>if (GetObjectA(object_id) == 0) {
|
||||
/// params.desired_heading = wire_heading; goto TurnToHeading; }</c>)
|
||||
/// — the resolved-object path never reads it.</item>
|
||||
/// <item>TurnToParameters (0xc bytes, exact retail order —
|
||||
/// <c>MovementParameters::UnPackNet</c> 3-dword TurnTo form, decomp
|
||||
/// §2g): u32 <c>Bitfield</c>, f32 <c>Speed</c>, f32
|
||||
/// <c>DesiredHeading</c>. Present for BOTH type 8 and type 9 (type 9
|
||||
/// has no guid/WireHeading head — <see cref="TargetGuid"/> and
|
||||
/// <see cref="WireHeading"/> are null).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Feeds <see cref="AcDream.Core.Physics.Motion.MovementParameters.FromWireTurnTo"/>
|
||||
/// at the (future) App-layer consumer — this record stays wire-primitive
|
||||
/// (no domain-object construction in the Net-layer parser).
|
||||
/// </summary>
|
||||
public readonly record struct TurnToPathData(
|
||||
uint? TargetGuid,
|
||||
float? WireHeading,
|
||||
uint Bitfield,
|
||||
float Speed,
|
||||
float DesiredHeading);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -532,11 +625,13 @@ public static class CreateObject
|
|||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
|
||||
|
||||
// 9 sequence timestamps, always present at end of PhysicsData.
|
||||
// Indices per holtburger: 0=position, 4=teleport, 5=serverControl,
|
||||
// 6=forcePosition, 8=instance.
|
||||
// PhysicsTimeStamp enum order (acclient.h:6084; ACE
|
||||
// WorldObject_Networking.cs:411-420): 0=position, 1=movement,
|
||||
// 4=teleport, 5=serverControl, 6=forcePosition, 8=instance.
|
||||
if (body.Length - pos < 9 * 2) return PartialResult();
|
||||
var seqSpan = body.Slice(pos, 9 * 2);
|
||||
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
|
||||
ushort movementSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(1 * 2));
|
||||
ushort teleportSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(4 * 2));
|
||||
ushort serverControlSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(5 * 2));
|
||||
ushort forcePositionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(6 * 2));
|
||||
|
|
@ -849,6 +944,7 @@ public static class CreateObject
|
|||
return new Parsed(guid, position, setupTableId, animParts,
|
||||
textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId,
|
||||
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq,
|
||||
movementSeq,
|
||||
physicsState, objectDescriptionFlags,
|
||||
friction, elasticity,
|
||||
IconId: iconId,
|
||||
|
|
|
|||
|
|
@ -9,18 +9,27 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// the extent + velocity to validate the jump and replicate it to nearby
|
||||
/// clients.
|
||||
///
|
||||
/// Wire layout (from holtburger JumpActionData::pack):
|
||||
/// u32 0xF7B1 (GameAction envelope)
|
||||
/// u32 sequence
|
||||
/// u32 0xF61B (Jump sub-opcode)
|
||||
/// f32 extent (0.0–1.0 charge power)
|
||||
/// f32 velocity.x, f32 velocity.y, f32 velocity.z
|
||||
/// u16 instanceSequence
|
||||
/// u16 serverControlSequence
|
||||
/// u16 teleportSequence
|
||||
/// u16 forcePositionSequence
|
||||
/// u32 objectGuid (0 for normal jump)
|
||||
/// u32 spellId (0 for normal jump)
|
||||
/// <para>
|
||||
/// Wire layout — ports retail's <c>JumpPack::Pack</c> verbatim
|
||||
/// (<c>0x00516d10</c>, decomp lines ~284934-284963). Confirmed against the
|
||||
/// Ghidra decompile-by-address bridge during this slice (2026-06-30):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>GameAction envelope</b>: u32 0xF7B1, u32 sequence, u32 0xF61B</item>
|
||||
/// <item><b>f32 extent</b> (0.0-1.0 charge power)</item>
|
||||
/// <item><b>f32 velocity.x, velocity.y, velocity.z</b></item>
|
||||
/// <item><b>Position::Pack</b>: u32 cellId, f32 x, f32 y, f32 z,
|
||||
/// f32 qw, f32 qx, f32 qy, f32 qz (32 bytes)</item>
|
||||
/// <item><b>Sequences</b>: u16 instance, u16 serverControl,
|
||||
/// u16 teleport, u16 forcePosition</item>
|
||||
/// <item><b>Align to 4 bytes</b></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>D4 fix.</b> Retail's <c>JumpPack</c> does NOT pack an objectGuid or a
|
||||
/// spellId, and DOES pack the full <c>Position</c> — the pre-slice code had
|
||||
/// it backwards (two spurious trailing zero u32s, no Position at all).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class JumpAction
|
||||
{
|
||||
|
|
@ -28,29 +37,45 @@ public static class JumpAction
|
|||
public const uint JumpOpcode = 0xF61Bu;
|
||||
|
||||
public static byte[] Build(
|
||||
uint gameActionSequence,
|
||||
float extent,
|
||||
Vector3 velocity,
|
||||
ushort instanceSequence,
|
||||
ushort serverControlSequence,
|
||||
ushort teleportSequence,
|
||||
ushort forcePositionSequence)
|
||||
uint gameActionSequence,
|
||||
float extent,
|
||||
Vector3 velocity,
|
||||
uint cellId,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
ushort instanceSequence,
|
||||
ushort serverControlSequence,
|
||||
ushort teleportSequence,
|
||||
ushort forcePositionSequence)
|
||||
{
|
||||
var w = new PacketWriter(48);
|
||||
var w = new PacketWriter(80);
|
||||
|
||||
w.WriteUInt32(GameActionOpcode);
|
||||
w.WriteUInt32(gameActionSequence);
|
||||
w.WriteUInt32(JumpOpcode);
|
||||
|
||||
w.WriteFloat(extent);
|
||||
w.WriteFloat(velocity.X);
|
||||
w.WriteFloat(velocity.Y);
|
||||
w.WriteFloat(velocity.Z);
|
||||
|
||||
// --- Position::Pack (0x005a9640): cellId + Frame::Pack (32 bytes) ---
|
||||
w.WriteUInt32(cellId);
|
||||
w.WriteFloat(position.X);
|
||||
w.WriteFloat(position.Y);
|
||||
w.WriteFloat(position.Z);
|
||||
// Quaternion wire order: W, X, Y, Z
|
||||
w.WriteFloat(rotation.W);
|
||||
w.WriteFloat(rotation.X);
|
||||
w.WriteFloat(rotation.Y);
|
||||
w.WriteFloat(rotation.Z);
|
||||
|
||||
w.WriteUInt16(instanceSequence);
|
||||
w.WriteUInt16(serverControlSequence);
|
||||
w.WriteUInt16(teleportSequence);
|
||||
w.WriteUInt16(forcePositionSequence);
|
||||
w.WriteUInt32(0); // objectGuid — 0 for normal jump
|
||||
w.WriteUInt32(0); // spellId — 0 for normal jump
|
||||
|
||||
w.AlignTo4();
|
||||
|
||||
return w.ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Net.Packets;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
|
|
@ -11,61 +12,39 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// state and to drive interpolated position for other nearby clients.
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (ported from
|
||||
/// <c>references/holtburger/crates/holtburger-protocol/src/messages/movement/actions.rs</c>
|
||||
/// <c>MoveToStateActionData::pack</c> and
|
||||
/// <c>types.rs RawMotionState::pack</c>):
|
||||
/// Wire layout — ports retail's <c>MoveToStatePack::Pack</c> verbatim
|
||||
/// (<c>0x005168f0</c>, decomp lines ~284694-284722). Confirmed against the
|
||||
/// Ghidra decompile-by-address bridge during this slice (2026-06-30):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>GameAction envelope</b>: u32 0xF7B1, u32 sequence, u32 0xF61C</item>
|
||||
/// <item><b>RawMotionState</b>: u32 packed_flags (bits 0-10 = flag bits,
|
||||
/// bits 11-31 = command list length), then conditional u32/f32 fields
|
||||
/// in flag-bit order (see <c>RawMotionFlags</c> in types.rs)</item>
|
||||
/// <item><b>WorldPosition</b>: u32 cellId, f32 x, f32 y, f32 z,
|
||||
/// f32 rotW, f32 rotX, f32 rotY, f32 rotZ</item>
|
||||
/// <item><b>RawMotionState::Pack</b>: see <see cref="RawMotionStatePacker"/>
|
||||
/// — default-difference flags dword + conditional fields + actions.</item>
|
||||
/// <item><b>Position::Pack</b>: u32 cellId, f32 x, f32 y, f32 z,
|
||||
/// f32 qw, f32 qx, f32 qy, f32 qz (32 bytes)</item>
|
||||
/// <item><b>Sequences</b>: u16 instance, u16 serverControl,
|
||||
/// u16 teleport, u16 forcePosition</item>
|
||||
/// <item><b>Contact byte</b>: u8 (1 = on ground, 0 = airborne)</item>
|
||||
/// <item><b>Trailing byte</b>: <c>(standingLongjump ? 0x02 : 0) |
|
||||
/// (contact ? 0x01 : 0)</c> — <c>MoveToStatePack::Pack</c> trailing
|
||||
/// byte expression <c>(longjump_mode == 0) - 1U & 2 |
|
||||
/// contact != 0</c>.</item>
|
||||
/// <item><b>Align to 4 bytes</b></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// The command list length is packed into bits 11-31 of the flags dword.
|
||||
/// We always send 0 commands (no discrete motion events), so those bits stay 0.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class MoveToState
|
||||
{
|
||||
public const uint GameActionOpcode = 0xF7B1u;
|
||||
public const uint MoveToStateAction = 0xF61Cu;
|
||||
|
||||
// RawMotionFlags bit positions (from holtburger types.rs)
|
||||
private const uint FlagCurrentHoldKey = 0x001u;
|
||||
private const uint FlagCurrentStyle = 0x002u;
|
||||
private const uint FlagForwardCommand = 0x004u;
|
||||
private const uint FlagForwardHoldKey = 0x008u;
|
||||
private const uint FlagForwardSpeed = 0x010u;
|
||||
private const uint FlagSidestepCommand = 0x020u;
|
||||
private const uint FlagSidestepHoldKey = 0x040u;
|
||||
private const uint FlagSidestepSpeed = 0x080u;
|
||||
private const uint FlagTurnCommand = 0x100u;
|
||||
private const uint FlagTurnHoldKey = 0x200u;
|
||||
private const uint FlagTurnSpeed = 0x400u;
|
||||
|
||||
/// <summary>
|
||||
/// Build a MoveToState GameAction body.
|
||||
/// </summary>
|
||||
/// <param name="gameActionSequence">Monotonically increasing counter from
|
||||
/// <see cref="WorldSession.NextGameActionSequence"/>.</param>
|
||||
/// <param name="forwardCommand">Raw motion command (u32 in AC's command space),
|
||||
/// e.g. 0x45000005 = WalkForward. Null = no forward command.</param>
|
||||
/// <param name="forwardSpeed">Normalised speed scalar (0.0-1.0). Only written
|
||||
/// when <paramref name="forwardCommand"/> is non-null.</param>
|
||||
/// <param name="sidestepCommand">Sidestep command or null.</param>
|
||||
/// <param name="sidestepSpeed">Sidestep speed or null.</param>
|
||||
/// <param name="turnCommand">Turn command or null.</param>
|
||||
/// <param name="turnSpeed">Turn speed or null.</param>
|
||||
/// <param name="holdKey">Hold-key state (1=None, 2=Run). Null = omit.</param>
|
||||
/// <param name="rawMotionState">Complete raw-motion snapshot, matching
|
||||
/// retail's <c>CPhysicsObj::InqRawMotionState()</c>. Fields equal to
|
||||
/// <see cref="RawMotionState.Default"/> are omitted from the wire
|
||||
/// (see <see cref="RawMotionStatePacker"/>).</param>
|
||||
/// <param name="cellId">Landblock cell ID (u32).</param>
|
||||
/// <param name="position">World-space position relative to the landblock.</param>
|
||||
/// <param name="rotation">Rotation quaternion. AC wire order is W, X, Y, Z.</param>
|
||||
|
|
@ -73,27 +52,22 @@ public static class MoveToState
|
|||
/// <param name="serverControlSequence">Server-control sequence number.</param>
|
||||
/// <param name="teleportSequence">Teleport sequence number.</param>
|
||||
/// <param name="forcePositionSequence">Force-position sequence number.</param>
|
||||
/// <param name="contactLongJump">1 if the character is on the ground, 0 if airborne.</param>
|
||||
/// <param name="contact">True if the character is on the ground.</param>
|
||||
/// <param name="standingLongjump">True during a standing (charged,
|
||||
/// stationary) longjump wind-up. Not yet implemented in acdream —
|
||||
/// callers pass <c>false</c> honestly until that feature lands.</param>
|
||||
public static byte[] Build(
|
||||
uint gameActionSequence,
|
||||
uint? forwardCommand,
|
||||
float? forwardSpeed,
|
||||
uint? sidestepCommand,
|
||||
float? sidestepSpeed,
|
||||
uint? turnCommand,
|
||||
float? turnSpeed,
|
||||
uint? holdKey,
|
||||
uint cellId,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
ushort instanceSequence,
|
||||
ushort serverControlSequence,
|
||||
ushort teleportSequence,
|
||||
ushort forcePositionSequence,
|
||||
byte contactLongJump = 1,
|
||||
uint? forwardHoldKey = null,
|
||||
uint? sidestepHoldKey = null,
|
||||
uint? turnHoldKey = null)
|
||||
uint gameActionSequence,
|
||||
RawMotionState rawMotionState,
|
||||
uint cellId,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
ushort instanceSequence,
|
||||
ushort serverControlSequence,
|
||||
ushort teleportSequence,
|
||||
ushort forcePositionSequence,
|
||||
bool contact = true,
|
||||
bool standingLongjump = false)
|
||||
{
|
||||
var w = new PacketWriter(128);
|
||||
|
||||
|
|
@ -102,43 +76,10 @@ public static class MoveToState
|
|||
w.WriteUInt32(gameActionSequence);
|
||||
w.WriteUInt32(MoveToStateAction);
|
||||
|
||||
// --- RawMotionState ---
|
||||
// Build the flags word. Command list length (bits 11-31) is always 0.
|
||||
// Field order matches holtburger's RawMotionState::pack — for any axis
|
||||
// where we send a COMMAND + SPEED, retail expects the matching
|
||||
// *_HOLD_KEY to accompany them (see holtburger's
|
||||
// build_motion_state_raw_motion_state). Without the per-axis hold
|
||||
// keys the server gets the flags but can't classify the input as a
|
||||
// continuously-held key, so other players see the character sliding
|
||||
// forward without an animation cycle.
|
||||
uint flags = 0u;
|
||||
if (holdKey.HasValue) flags |= FlagCurrentHoldKey;
|
||||
if (forwardCommand.HasValue) flags |= FlagForwardCommand;
|
||||
if (forwardHoldKey.HasValue) flags |= FlagForwardHoldKey;
|
||||
if (forwardSpeed.HasValue) flags |= FlagForwardSpeed;
|
||||
if (sidestepCommand.HasValue) flags |= FlagSidestepCommand;
|
||||
if (sidestepHoldKey.HasValue) flags |= FlagSidestepHoldKey;
|
||||
if (sidestepSpeed.HasValue) flags |= FlagSidestepSpeed;
|
||||
if (turnCommand.HasValue) flags |= FlagTurnCommand;
|
||||
if (turnHoldKey.HasValue) flags |= FlagTurnHoldKey;
|
||||
if (turnSpeed.HasValue) flags |= FlagTurnSpeed;
|
||||
// --- RawMotionState::Pack (0x0051ed10) ---
|
||||
RawMotionStatePacker.Pack(w, rawMotionState);
|
||||
|
||||
w.WriteUInt32(flags); // bits 0-10 = flags, bits 11-31 = 0 (no command list)
|
||||
|
||||
// Conditional fields in RawMotionFlags bit order:
|
||||
if (holdKey.HasValue) w.WriteUInt32(holdKey.Value);
|
||||
// FlagCurrentStyle (0x2): not sent — we don't track stance changes here
|
||||
if (forwardCommand.HasValue) w.WriteUInt32(forwardCommand.Value);
|
||||
if (forwardHoldKey.HasValue) w.WriteUInt32(forwardHoldKey.Value);
|
||||
if (forwardSpeed.HasValue) w.WriteFloat(forwardSpeed.Value);
|
||||
if (sidestepCommand.HasValue) w.WriteUInt32(sidestepCommand.Value);
|
||||
if (sidestepHoldKey.HasValue) w.WriteUInt32(sidestepHoldKey.Value);
|
||||
if (sidestepSpeed.HasValue) w.WriteFloat(sidestepSpeed.Value);
|
||||
if (turnCommand.HasValue) w.WriteUInt32(turnCommand.Value);
|
||||
if (turnHoldKey.HasValue) w.WriteUInt32(turnHoldKey.Value);
|
||||
if (turnSpeed.HasValue) w.WriteFloat(turnSpeed.Value);
|
||||
|
||||
// --- WorldPosition (32 bytes) ---
|
||||
// --- Position::Pack (0x005a9640): cellId + Frame::Pack (32 bytes) ---
|
||||
w.WriteUInt32(cellId);
|
||||
w.WriteFloat(position.X);
|
||||
w.WriteFloat(position.Y);
|
||||
|
|
@ -155,8 +96,10 @@ public static class MoveToState
|
|||
w.WriteUInt16(teleportSequence);
|
||||
w.WriteUInt16(forcePositionSequence);
|
||||
|
||||
// --- Contact byte + 4-byte align ---
|
||||
w.WriteByte(contactLongJump);
|
||||
// --- Trailing byte (MoveToStatePack::Pack, 0x005168f0): ---
|
||||
// ((longjump_mode != 0) ? 0x02 : 0) | (contact != 0 ? 0x01 : 0)
|
||||
byte trailing = (byte)((standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0));
|
||||
w.WriteByte(trailing);
|
||||
w.AlignTo4();
|
||||
|
||||
return w.ToArray();
|
||||
|
|
|
|||
98
src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs
Normal file
98
src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using AcDream.Core.Net.Packets;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Ports retail's <c>RawMotionState::Pack</c> verbatim
|
||||
/// (<c>0x0051ed10</c>, decomp lines ~293761-294013; bitfield layout
|
||||
/// <c>acclient.h RawMotionState::PackBitfield</c>, line 46474). Confirmed
|
||||
/// against the Ghidra decompile-by-address bridge during this slice
|
||||
/// (2026-06-30).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>D1 fix.</b> Retail does NOT set a field's bit merely because the
|
||||
/// caller supplied a value — it compares every field against its retail
|
||||
/// DEFAULT (<see cref="RawMotionState.Default"/>) and sets the bit (and
|
||||
/// emits the field) only when the live value DIFFERS. This packer mirrors
|
||||
/// that default-difference comparison exactly, field by field, in bit
|
||||
/// order.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Flags dword layout (bits 0-15 only; bits 16-31 are unused — retail's
|
||||
/// <c>num_actions</c> is a 5-bit field occupying bits 11-15, not "the rest
|
||||
/// of the dword"):
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item><term>0x001</term><description>current_holdkey != HoldKey.None</description></item>
|
||||
/// <item><term>0x002</term><description>current_style != 0x8000003D</description></item>
|
||||
/// <item><term>0x004</term><description>forward_command != 0x41000003</description></item>
|
||||
/// <item><term>0x008</term><description>forward_holdkey != HoldKey.Invalid</description></item>
|
||||
/// <item><term>0x010</term><description>forward_speed != 1.0f</description></item>
|
||||
/// <item><term>0x020</term><description>sidestep_command != 0</description></item>
|
||||
/// <item><term>0x040</term><description>sidestep_holdkey != HoldKey.Invalid</description></item>
|
||||
/// <item><term>0x080</term><description>sidestep_speed != 1.0f</description></item>
|
||||
/// <item><term>0x100</term><description>turn_command != 0</description></item>
|
||||
/// <item><term>0x200</term><description>turn_holdkey != HoldKey.Invalid</description></item>
|
||||
/// <item><term>0x400</term><description>turn_speed != 1.0f</description></item>
|
||||
/// <item><term>0xF800</term><description>num_actions (bits 11-15, count not values)</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class RawMotionStatePacker
|
||||
{
|
||||
private const uint FlagCurrentHoldKey = 0x001u;
|
||||
private const uint FlagCurrentStyle = 0x002u;
|
||||
private const uint FlagForwardCommand = 0x004u;
|
||||
private const uint FlagForwardHoldKey = 0x008u;
|
||||
private const uint FlagForwardSpeed = 0x010u;
|
||||
private const uint FlagSidestepCommand = 0x020u;
|
||||
private const uint FlagSidestepHoldKey = 0x040u;
|
||||
private const uint FlagSidestepSpeed = 0x080u;
|
||||
private const uint FlagTurnCommand = 0x100u;
|
||||
private const uint FlagTurnHoldKey = 0x200u;
|
||||
private const uint FlagTurnSpeed = 0x400u;
|
||||
private const int NumActionsShift = 11;
|
||||
|
||||
public static void Pack(PacketWriter w, RawMotionState state)
|
||||
{
|
||||
var defaults = RawMotionState.Default;
|
||||
|
||||
uint flags = 0u;
|
||||
if (state.CurrentHoldKey != defaults.CurrentHoldKey) flags |= FlagCurrentHoldKey;
|
||||
if (state.CurrentStyle != defaults.CurrentStyle) flags |= FlagCurrentStyle;
|
||||
if (state.ForwardCommand != defaults.ForwardCommand) flags |= FlagForwardCommand;
|
||||
if (state.ForwardHoldKey != defaults.ForwardHoldKey) flags |= FlagForwardHoldKey;
|
||||
if (state.ForwardSpeed != defaults.ForwardSpeed) flags |= FlagForwardSpeed;
|
||||
if (state.SidestepCommand != defaults.SidestepCommand) flags |= FlagSidestepCommand;
|
||||
if (state.SidestepHoldKey != defaults.SidestepHoldKey) flags |= FlagSidestepHoldKey;
|
||||
if (state.SidestepSpeed != defaults.SidestepSpeed) flags |= FlagSidestepSpeed;
|
||||
if (state.TurnCommand != defaults.TurnCommand) flags |= FlagTurnCommand;
|
||||
if (state.TurnHoldKey != defaults.TurnHoldKey) flags |= FlagTurnHoldKey;
|
||||
if (state.TurnSpeed != defaults.TurnSpeed) flags |= FlagTurnSpeed;
|
||||
|
||||
int numActions = state.Actions.Count;
|
||||
flags |= (uint)(numActions << NumActionsShift);
|
||||
|
||||
w.WriteUInt32(flags);
|
||||
|
||||
if ((flags & FlagCurrentHoldKey) != 0) w.WriteUInt32((uint)state.CurrentHoldKey);
|
||||
if ((flags & FlagCurrentStyle) != 0) w.WriteUInt32(state.CurrentStyle);
|
||||
if ((flags & FlagForwardCommand) != 0) w.WriteUInt32(state.ForwardCommand);
|
||||
if ((flags & FlagForwardHoldKey) != 0) w.WriteUInt32((uint)state.ForwardHoldKey);
|
||||
if ((flags & FlagForwardSpeed) != 0) w.WriteFloat(state.ForwardSpeed);
|
||||
if ((flags & FlagSidestepCommand) != 0) w.WriteUInt32(state.SidestepCommand);
|
||||
if ((flags & FlagSidestepHoldKey) != 0) w.WriteUInt32((uint)state.SidestepHoldKey);
|
||||
if ((flags & FlagSidestepSpeed) != 0) w.WriteFloat(state.SidestepSpeed);
|
||||
if ((flags & FlagTurnCommand) != 0) w.WriteUInt32(state.TurnCommand);
|
||||
if ((flags & FlagTurnHoldKey) != 0) w.WriteUInt32((uint)state.TurnHoldKey);
|
||||
if ((flags & FlagTurnSpeed) != 0) w.WriteFloat(state.TurnSpeed);
|
||||
|
||||
foreach (var action in state.Actions)
|
||||
{
|
||||
w.WriteUInt16(action.Command);
|
||||
ushort stampWord = (ushort)((action.Stamp & 0x7FFF) | (action.Autonomous ? 0x8000 : 0));
|
||||
w.WriteUInt16(stampWord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -57,10 +70,21 @@ public static class UpdateMotion
|
|||
/// command is nullable because the <c>ForwardCommand</c> flag may be
|
||||
/// unset in the InterpretedMotionState; the stance is always present
|
||||
/// (even if 0, meaning "no specific stance").
|
||||
///
|
||||
/// <para>L.2g S1 (DEV-6): the three staleness stamps + autonomy flag are
|
||||
/// exposed for the retail gate (<see cref="AcDream.Core.Physics"/>
|
||||
/// <c>MotionSequenceGate</c>) — retail compares them against
|
||||
/// <c>update_times[INSTANCE_TS/MOVEMENT_TS/SERVER_CONTROLLED_MOVE_TS]</c>
|
||||
/// and stores <c>last_move_was_autonomous</c>
|
||||
/// (<c>CPhysics::SetObjectMovement</c> 0x00509690).</para>
|
||||
/// </summary>
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerMotionState MotionState);
|
||||
CreateObject.ServerMotionState MotionState,
|
||||
ushort InstanceSequence,
|
||||
ushort MovementSequence,
|
||||
ushort ServerControlSequence,
|
||||
bool IsAutonomous);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a reassembled UpdateMotion body. <paramref name="body"/> must
|
||||
|
|
@ -82,8 +106,11 @@ public static class UpdateMotion
|
|||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
|
||||
// ObjectInstance sequence (u16) — tracked but not used for pose.
|
||||
// ObjectInstance sequence (u16) — retail's dispatch-level
|
||||
// INSTANCE_TS staleness gate compares this against the object's
|
||||
// known incarnation (DispatchSmartBoxEvent case 0xF74C).
|
||||
if (body.Length - pos < 2) return null;
|
||||
ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
|
||||
// MovementData header: u16 movementSequence, u16 serverControlSequence,
|
||||
|
|
@ -102,13 +129,34 @@ public static class UpdateMotion
|
|||
// Previous version mistakenly reserved 8 bytes here, which shifted
|
||||
// every subsequent field by 2 and made every remote-char UpdateMotion
|
||||
// decode as garbage (stance read from the packed-flags dword).
|
||||
//
|
||||
// L.2g S1 (DEV-6): the header fields feed retail's
|
||||
// CPhysics::SetObjectMovement staleness gates + the
|
||||
// last_move_was_autonomous store, so parse them instead of
|
||||
// skipping.
|
||||
if (body.Length - pos < 6) return null;
|
||||
pos += 6;
|
||||
ushort movementSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
ushort serverControlSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
bool isAutonomous = body[pos] != 0;
|
||||
pos += 2; // u8 isAutonomous + Align(4) pad byte
|
||||
|
||||
// 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
|
||||
// carried on ServerMotionState.StandingLongJump (R5-V4) and
|
||||
// consumed at the GameWindow mt-0 tails — retail's
|
||||
// `unpack_movement` case 0 writes it onto
|
||||
// `motion_interpreter->standing_longjump` (§2f @0052458e,
|
||||
// UNCONDITIONAL — an absent flag clears it).
|
||||
byte motionFlags = body[pos]; pos += 1;
|
||||
ushort currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
|
||||
|
|
@ -118,7 +166,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;
|
||||
|
|
@ -131,6 +179,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)
|
||||
|
|
@ -139,7 +189,7 @@ public static class UpdateMotion
|
|||
// MovementInvalid branch, just reached via the header'd path.
|
||||
// Includes the Commands list (MotionItem[]) that carries
|
||||
// Actions, emotes, and other one-shots not in ForwardCommand.
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
uint packed = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
uint flags = packed & 0x7Fu;
|
||||
|
|
@ -162,13 +212,13 @@ public static class UpdateMotion
|
|||
|
||||
if ((flags & 0x1u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
if ((flags & 0x2u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
forwardCommand = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
|
|
@ -223,6 +273,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)
|
||||
|
|
@ -236,6 +303,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,
|
||||
|
|
@ -244,7 +319,14 @@ public static class UpdateMotion
|
|||
moveToParameters,
|
||||
moveToSpeed,
|
||||
moveToRunRate,
|
||||
moveToPath));
|
||||
moveToPath,
|
||||
turnToPath,
|
||||
stickyObjectGuid,
|
||||
// R5-V4: MotionFlags 0x2 — consumed at the GameWindow mt-0
|
||||
// tails (retail unpack_movement @0052458e writes it onto
|
||||
// minterp->standing_longjump unconditionally).
|
||||
(motionFlags & 0x2) != 0),
|
||||
instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -326,6 +408,73 @@ public static class UpdateMotion
|
|||
minDistance,
|
||||
failDistance,
|
||||
walkRunThreshold,
|
||||
desiredHeading,
|
||||
movementParameters ?? 0u); // R4-V4: the raw flags dword -> FromWire
|
||||
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