feat(net): port retail physics spawn and event timestamps

Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client.

Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 00:22:17 +02:00
parent d53fe30ffe
commit 8a5d77f7f4
50 changed files with 3809 additions and 649 deletions

View file

@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Net.Messages;
@ -10,8 +11,9 @@ namespace AcDream.Core.Net.Messages;
/// weenies like the Holtburg foundry statue) in the client's loaded area.
///
/// <para>
/// acdream's parser extracts only the fields needed to hand the spawn off
/// to <c>IGameState</c>:
/// The parser preserves the complete PhysicsDesc needed to construct a retail
/// physics object, while retaining legacy convenience fields during the
/// LiveEntityRuntime migration:
/// </para>
/// <list type="bullet">
/// <item><b>GUID</b> — always at the start of the body (after the opcode).</item>
@ -25,13 +27,9 @@ namespace AcDream.Core.Net.Messages;
/// </list>
///
/// <para>
/// Most other fields (extended weenie header, object description, motion tables,
/// palettes, texture overrides, animation frames, velocity, ...) are
/// consumed-but-ignored so the parse position ends up wherever the
/// client-side caller wanted — a <c>Parse</c> call doesn't need to reach
/// the end of the body to return useful output. We read through the fixed
/// WeenieHeader prefix for Name/ItemType, then stop before optional header
/// tails.
/// Every PhysicsDesc field is captured, including presence-preserving zero
/// values and all nine timestamps. The PublicWeenieDesc parser continues to
/// retain its supported presentation/gameplay subset.
/// </para>
///
/// <para>
@ -120,7 +118,7 @@ public static class CreateObject
ushort ServerControlSequence = 0,
ushort ForcePositionSequence = 0,
// L.2g S1 (DEV-6): ObjectMovement stamp (timestamp block index 1)
// seeds MotionSequenceGate's MOVEMENT_TS at spawn.
// seeds PhysicsTimestampGate's MOVEMENT_TS at spawn.
ushort MovementSequence = 0,
// Parent/placement bootstrap for equipped child objects. These are
// the CreateObject equivalents of ParentEvent 0xF749.
@ -208,7 +206,11 @@ public static class CreateObject
uint? PetOwnerId = null,
// PublicWeenieDesc._ammoType, gated by WeenieHeader flag 0x100.
// AMMO_NONE is the explicit wire value zero; null means absent.
ushort? AmmoType = null);
ushort? AmmoType = null,
// Complete immutable PhysicsDesc projection. Kept alongside the
// legacy convenience fields while live-entity ownership migrates to
// LiveEntityRuntime in Step 2.
PhysicsSpawnData? Physics = null);
/// <summary>
/// The relevant subset of the server-sent <c>MovementData</c> /
@ -479,8 +481,8 @@ public static class CreateObject
/// Parse a reassembled CreateObject body. <paramref name="body"/> must
/// start with the 4-byte opcode. Returns <c>null</c> if the body is
/// malformed (truncated field); returns a populated <see cref="Parsed"/>
/// on success. The parser stops at the end of PhysicsData; subsequent
/// weenie-header fields are deliberately not consumed.
/// on success after consuming the supported PhysicsDesc and public
/// WeenieHeader fields.
/// </summary>
public static Parsed? TryParse(ReadOnlySpan<byte> body)
{
@ -492,6 +494,17 @@ public static class CreateObject
float? objScale = null;
ServerMotionState? motionState = null;
uint? motionTableId = null;
PhysicsMovementData? movement = null;
uint? soundTableId = null;
uint? physicsScriptTableId = null;
ReadOnlyMemory<PhysicsAttachment>? children = null;
float? translucency = null;
Vector3? velocity = null;
Vector3? acceleration = null;
Vector3? angularVelocity = null;
uint? defaultScriptType = null;
float? defaultScriptIntensity = null;
PhysicsSpawnData? physics = null;
// Commit A 2026-04-29 — live-entity collision plumbing. PhysicsState
// (acclient.h:2815) was previously skipped at line ~337; the PWD
// _bitfield (acclient.h:6431-6463) was previously discarded as
@ -547,11 +560,16 @@ public static class CreateObject
{
if (body.Length - pos < (int)movementLen) return null;
int movementStart = pos;
motionState = TryParseMovementData(body.Slice(movementStart, (int)movementLen));
ReadOnlySpan<byte> movementBytes = body.Slice(movementStart, (int)movementLen);
motionState = TryParseMovementData(movementBytes);
pos = movementStart + (int)movementLen;
if (body.Length - pos < 4) return null;
pos += 4; // isAutonomous u32
bool isAutonomous = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)) != 0;
pos += 4;
movement = new PhysicsMovementData(movementBytes.ToArray(), motionState, isAutonomous);
}
else
movement = new PhysicsMovementData(ReadOnlyMemory<byte>.Empty, null, null);
}
else if ((physicsFlags & PhysicsDescriptionFlag.AnimationFrame) != 0)
{
@ -585,12 +603,14 @@ public static class CreateObject
if ((physicsFlags & PhysicsDescriptionFlag.STable) != 0)
{
if (body.Length - pos < 4) return null;
soundTableId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
}
if ((physicsFlags & PhysicsDescriptionFlag.PeTable) != 0)
{
if (body.Length - pos < 4) return null;
physicsScriptTableId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
}
@ -616,19 +636,27 @@ public static class CreateObject
if (body.Length - pos < 4) return null;
int childCount = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(pos));
pos += 4;
if (childCount < 0 || childCount > 1024) return PartialResult();
if (childCount < 0 || childCount > 1024) return null;
if (body.Length - pos < childCount * 8) return null;
pos += childCount * 8; // each child = guid u32 + locationId u32
var parsedChildren = new PhysicsAttachment[childCount];
for (int i = 0; i < parsedChildren.Length; i++)
{
parsedChildren[i] = new PhysicsAttachment(
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)),
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 4)));
pos += 8;
}
children = parsedChildren;
}
if ((physicsFlags & PhysicsDescriptionFlag.ObjScale) != 0)
{
if (body.Length - pos < 4) return PartialResult();
if (body.Length - pos < 4) return null;
objScale = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
}
if ((physicsFlags & PhysicsDescriptionFlag.Friction) != 0)
{
if (body.Length - pos < 4) return PartialResult();
if (body.Length - pos < 4) return null;
friction = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
}
@ -641,31 +669,89 @@ public static class CreateObject
// Was previously dropped — every object got the default
// 0.05f, so server-set bouncier surfaces felt identical to
// walls.
if (body.Length - pos < 4) return PartialResult();
if (body.Length - pos < 4) return null;
elasticity = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
}
if ((physicsFlags & PhysicsDescriptionFlag.Translucency) != 0) pos += 4;
if ((physicsFlags & PhysicsDescriptionFlag.Velocity) != 0) pos += 12; // vec3
if ((physicsFlags & PhysicsDescriptionFlag.Acceleration) != 0) pos += 12;
if ((physicsFlags & PhysicsDescriptionFlag.Omega) != 0) pos += 12;
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScript) != 0) pos += 4;
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
if ((physicsFlags & PhysicsDescriptionFlag.Translucency) != 0)
{
if (body.Length - pos < 4) return null;
translucency = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
}
if ((physicsFlags & PhysicsDescriptionFlag.Velocity) != 0)
{
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
velocity = value;
}
if ((physicsFlags & PhysicsDescriptionFlag.Acceleration) != 0)
{
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
acceleration = value;
}
if ((physicsFlags & PhysicsDescriptionFlag.Omega) != 0)
{
if (!TryReadVector3(body, ref pos, out Vector3 value)) return null;
angularVelocity = value;
}
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScript) != 0)
{
if (body.Length - pos < 4) return null;
defaultScriptType = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
}
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0)
{
if (body.Length - pos < 4) return null;
defaultScriptIntensity = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
}
// 9 sequence timestamps, always present at end of PhysicsData.
// 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();
if (body.Length - pos < 9 * 2) return null;
var seqSpan = body.Slice(pos, 9 * 2);
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
ushort positionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(0 * 2));
ushort movementSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(1 * 2));
ushort stateSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(2 * 2));
ushort vectorSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(3 * 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));
ushort objDescSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(7 * 2));
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
pos += 9 * 2;
AlignTo4(ref pos);
if (pos > body.Length) return null;
var timestamps = new PhysicsTimestamps(
positionSeq, movementSeq, stateSeq, vectorSeq, teleportSeq,
serverControlSeq, forcePositionSeq, objDescSeq, instanceSeq);
physics = new PhysicsSpawnData(
RawState: physicsState.Value,
Position: position,
Movement: movement,
AnimationFrame: placementId,
SetupTableId: setupTableId,
MotionTableId: motionTableId,
SoundTableId: soundTableId,
PhysicsScriptTableId: physicsScriptTableId,
Parent: parentGuid.HasValue && parentLocation.HasValue
? new PhysicsAttachment(parentGuid.Value, parentLocation.Value)
: null,
Children: children,
Scale: objScale,
Friction: friction,
Elasticity: elasticity,
Translucency: translucency,
Velocity: velocity,
Acceleration: acceleration,
AngularVelocity: angularVelocity,
DefaultScriptType: defaultScriptType,
DefaultScriptIntensity: defaultScriptIntensity,
Timestamps: timestamps);
// --- WeenieHeader: read the fixed prefix fields we need. ---
// ACE WorldObject_Networking.SerializeCreateObject writes:
@ -1026,17 +1112,8 @@ public static class CreateObject
CombatUse: combatUse,
PluralName: pluralName,
PetOwnerId: petOwnerId,
AmmoType: ammoType);
// Local helper: if we ran out of fields past PhysicsData, still
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
Parsed PartialResult() => new(
guid, position, setupTableId, animParts,
textureChanges, subPalettes, basePaletteId, objScale, null, null, motionState, motionTableId,
PhysicsState: physicsState,
ObjectDescriptionFlags: objectDescriptionFlags,
Friction: friction,
Elasticity: elasticity);
AmmoType: ammoType,
Physics: physics);
}
catch
{
@ -1391,4 +1468,20 @@ public static class CreateObject
runRate = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
return true;
}
private static bool TryReadVector3(ReadOnlySpan<byte> body, ref int pos, out Vector3 value)
{
if (body.Length - pos < 12)
{
value = default;
return false;
}
value = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)),
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 4)),
BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 8)));
pos += 12;
return true;
}
}

View file

@ -17,23 +17,14 @@ public static class DeleteObject
{
public const uint Opcode = 0xF747u;
/// <param name="FromPickup">
/// <see langword="true"/> when this delete was sourced from a
/// <c>PickupEvent (0xF74A)</c> — the object left the 3-D world view but
/// the weenie record still exists (it is moving into a container).
/// <see langword="false"/> (default) when sourced from <c>DeleteObject
/// (0xF747)</c> — the weenie was destroyed and must be evicted from
/// <see cref="AcDream.Core.Items.ClientObjectTable"/>.
/// Retail two-table model: PickupEvent removes from <c>object_table</c>;
/// DeleteObject removes from <c>weenie_object_table</c>.
/// </param>
public readonly record struct Parsed(uint Guid, ushort InstanceSequence, bool FromPickup = false);
/// <summary>A true object-destruction event for one exact incarnation.</summary>
public readonly record struct Parsed(uint Guid, ushort InstanceSequence);
/// <summary>
/// Parse a 0xF747 body. <paramref name="body"/> must start with the
/// 4-byte opcode, matching every other parser in this namespace.
/// The returned <see cref="Parsed.FromPickup"/> is always <see langword="false"/>
/// — this parser handles true destroys only.
/// PickupEvent has a distinct parser and runtime event because it advances
/// POSITION_TS and retains the logical object.
/// </summary>
public static Parsed? TryParse(ReadOnlySpan<byte> body)
{
@ -46,6 +37,6 @@ public static class DeleteObject
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4));
ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(8, 2));
return new Parsed(guid, instanceSequence, FromPickup: false);
return new Parsed(guid, instanceSequence);
}
}

View file

@ -26,8 +26,8 @@ namespace AcDream.Core.Net.Messages;
/// <item>u32 opcode (0xF625)</item>
/// <item>u32 guid — target object</item>
/// <item>ModelData block — see <see cref="CreateObject.ReadModelData"/></item>
/// <item>u32 instanceSequence</item>
/// <item>u32 visualDescSequence</item>
/// <item>u16 instanceSequence</item>
/// <item>u16 visualDescSequence (OBJDESC_TS)</item>
/// </list>
/// </summary>
public static class ObjDescEvent
@ -35,11 +35,14 @@ public static class ObjDescEvent
public const uint Opcode = 0xF625u;
/// <summary>
/// One ObjDescEvent: target guid + the new ModelData. Sequence
/// counters are read but not surfaced (subscribers don't need them
/// — the event always carries the full new appearance).
/// One ObjDescEvent: target guid, new ModelData, and retail's packed
/// INSTANCE_TS/OBJDESC_TS pair.
/// </summary>
public readonly record struct Parsed(uint Guid, CreateObject.ModelData ModelData);
public readonly record struct Parsed(
uint Guid,
CreateObject.ModelData ModelData,
ushort InstanceSequence,
ushort ObjDescSequence);
/// <summary>
/// Parse an ObjDescEvent body (must start with the 4-byte opcode).
@ -61,10 +64,13 @@ public static class ObjDescEvent
var modelData = CreateObject.ReadModelData(body, ref pos);
// Trailing instanceSeq + visualDescSeq are read for completeness
// but not surfaced — subscribers re-render unconditionally on
// every event since each carries the full appearance.
return new Parsed(guid, modelData);
// PhysicsTimestampPack::UnPack 0x00516F50 is exactly two u16s.
// Reject a missing or overlong tail so cursor mistakes cannot turn
// into an apparently valid, ungated appearance update.
if (body.Length - pos != 4) return null;
ushort instance = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
ushort objDesc = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 2));
return new Parsed(guid, modelData, instance, objDesc);
}
catch
{

View file

@ -0,0 +1,64 @@
using System.Numerics;
using AcDream.Core.Physics;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// The nine retail <c>PhysicsTimeStamp</c> channels stored at the tail of a
/// <c>PhysicsDesc</c>. Field order is verbatim from <c>acclient.h:6084</c>.
/// </summary>
public readonly record struct PhysicsTimestamps(
ushort Position,
ushort Movement,
ushort State,
ushort Vector,
ushort Teleport,
ushort ServerControlledMove,
ushort ForcePosition,
ushort ObjDesc,
ushort Instance);
/// <summary>A parent or child attachment carried by <c>PhysicsDesc</c>.</summary>
public readonly record struct PhysicsAttachment(uint Guid, uint LocationId);
/// <summary>
/// A present Movement field. The wrapper is retained even when
/// <see cref="RawData"/> is empty so callers can distinguish absent Movement
/// from an explicitly present zero-length movement block.
/// </summary>
public readonly record struct PhysicsMovementData(
ReadOnlyMemory<byte> RawData,
CreateObject.ServerMotionState? MotionState,
bool? IsAutonomous);
/// <summary>
/// Immutable, lossless projection of the retail <c>PhysicsDesc</c> embedded in
/// CreateObject (0xF745). Nullable members preserve absent versus present-zero.
/// Network acceleration is retained for protocol fidelity; retail recalculates
/// normal acceleration after applying the final physics state.
/// </summary>
public readonly record struct PhysicsSpawnData(
uint RawState,
CreateObject.ServerPosition? Position,
PhysicsMovementData? Movement,
uint? AnimationFrame,
uint? SetupTableId,
uint? MotionTableId,
uint? SoundTableId,
uint? PhysicsScriptTableId,
PhysicsAttachment? Parent,
ReadOnlyMemory<PhysicsAttachment>? Children,
float? Scale,
float? Friction,
float? Elasticity,
float? Translucency,
Vector3? Velocity,
Vector3? Acceleration,
Vector3? AngularVelocity,
uint? DefaultScriptType,
float? DefaultScriptIntensity,
PhysicsTimestamps Timestamps)
{
/// <summary>The corrected retail flag view of <see cref="RawState"/>.</summary>
public PhysicsStateFlags State => (PhysicsStateFlags)RawState;
}

View file

@ -9,9 +9,9 @@ namespace AcDream.Core.Net.Messages;
/// ACE emits this from <c>Player_Tracking.RemoveTrackedObject(wo, fromPickup: true)</c>
/// when a player picks up a world item — distinguishes the despawn
/// from a generic <c>0xF747 DeleteObject</c> (timeout / death /
/// out-of-LOS). Downstream effect on the client view is the same
/// (remove the entity from the world), so <see cref="WorldSession"/>
/// routes both opcodes to the same <c>EntityDeleted</c> event.
/// out-of-LOS). Pickup removes only the object's world projection while
/// retaining its logical weenie and timestamp owner, so <see cref="WorldSession"/>
/// publishes it separately through <c>EntityPickedUp</c>.
/// </para>
///
/// <para>

View file

@ -0,0 +1,25 @@
using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Retail PlayScriptID (0xF754): play the supplied PhysicsScript DID directly
/// on the addressed object. This packet never performs a typed-table lookup.
/// Named-retail handler: <c>SmartBox::HandlePlayScriptID</c> 0x00452020.
/// </summary>
public readonly record struct PlayPhysicsScript(uint Guid, uint ScriptDid)
{
public const uint Opcode = 0xF754u;
public const int WireSize = 12;
public static PlayPhysicsScript? TryParse(ReadOnlySpan<byte> body)
{
if (body.Length != WireSize
|| BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode)
return null;
return new PlayPhysicsScript(
BinaryPrimitives.ReadUInt32LittleEndian(body[4..]),
BinaryPrimitives.ReadUInt32LittleEndian(body[8..]));
}
}

View file

@ -0,0 +1,31 @@
using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Retail PlayScriptType (0xF755): resolve the raw script type and exact
/// incoming intensity through the object's current PhysicsScriptTable.
/// Unknown type values and non-finite floats are retained losslessly for the
/// resolver to reject without corrupting packet delivery.
/// Named-retail handler: <c>SmartBox::HandlePlayScriptType</c> 0x00452070.
/// </summary>
public readonly record struct PlayPhysicsScriptType(
uint Guid,
uint RawScriptType,
float Intensity)
{
public const uint Opcode = 0xF755u;
public const int WireSize = 16;
public static PlayPhysicsScriptType? TryParse(ReadOnlySpan<byte> body)
{
if (body.Length != WireSize
|| BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode)
return null;
return new PlayPhysicsScriptType(
BinaryPrimitives.ReadUInt32LittleEndian(body[4..]),
BinaryPrimitives.ReadUInt32LittleEndian(body[8..]),
BinaryPrimitives.ReadSingleLittleEndian(body[12..]));
}
}

View file

@ -73,7 +73,7 @@ public static class UpdateMotion
///
/// <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>PhysicsTimestampGate</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>

View file

@ -27,8 +27,7 @@ namespace AcDream.Core.Net.Messages;
/// <item><b>Velocity</b> — 3xf32 if HasVelocity set</item>
/// <item><b>PlacementID</b> — u32 if HasPlacementID set</item>
/// <item><b>Four u16 sequence numbers</b> — instance, position, teleport,
/// forcePosition. We don't currently check these for freshness but
/// we must consume them to walk the buffer correctly.</item>
/// forcePosition. Runtime freshness gates consume all four.</item>
/// </list>
/// </summary>
public static class UpdatePosition
@ -66,6 +65,7 @@ public static class UpdatePosition
uint? PlacementId,
bool IsGrounded,
ushort InstanceSequence = 0,
ushort PositionSequence = 0,
ushort TeleportSequence = 0,
ushort ForcePositionSequence = 0);
@ -149,15 +149,12 @@ public static class UpdatePosition
}
// Four u16 sequence numbers: instance, position, teleport, forcePosition.
ushort instSeq = 0, teleSeq = 0, forceSeq = 0;
if (body.Length - pos >= 8)
{
instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
// pos+2 = positionSequence (not tracked by movement)
teleSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 4));
forceSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 6));
pos += 8;
}
if (body.Length - pos < 8) return null;
ushort instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
ushort posSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 2));
ushort teleSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 4));
ushort forceSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 6));
pos += 8;
var serverPos = new CreateObject.ServerPosition(
LandblockId: cellId,
@ -166,7 +163,7 @@ public static class UpdatePosition
return new Parsed(guid, serverPos, velocity, placementId,
IsGrounded: (flags & PositionFlags.IsGrounded) != 0,
instSeq, teleSeq, forceSeq);
instSeq, posSeq, teleSeq, forceSeq);
}
catch
{