acdream/src/AcDream.Core.Net/Messages/VectorUpdate.cs
Erik 0ebf0cad09 fix(net): VectorUpdate parser was reading guid from opcode bytes — remote jumps invisible
User report: "in ACdream client, when other client is jumping,
nothing happens at all".

Diagnostic [VU.recv] revealed the parser was reading
guid = 0x0000F74E (= the opcode itself) and velocity values in
the billions:

  [VU.recv] guid=0x0000F74E vel=(8589944832.00,0.00,0.00)
            isLocal=False hasRemote=False

WorldSession.ProcessDatagram passes the FULL reassembled body
including the 4-byte opcode at offset 0 — every other parser
in src/AcDream.Core.Net/Messages/ verifies the opcode word
before reading payload (UpdateMotion.TryParse:77,
UpdatePosition.TryParse, etc.). VectorUpdate.TryParse skipped
that step and read every field shifted four bytes early,
making the guid the opcode bytes and the velocities random
floats from later in the buffer. With guid=0xF74E never
matching any tracked entity, OnLiveVectorUpdated returned
early and remote jumps rendered nothing.

Fix: read + verify opcode at offset 0 in TryParse, then read
guid at offset 4, velocity at 8/12/16, omega at 20/24/28,
sequences at 32/34. Body length now 4 (opcode) + 32 (payload).

Tests stay 1222 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:51:36 +02:00

82 lines
3.5 KiB
C#

using System.Buffers.Binary;
using System.Numerics;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Inbound <c>VectorUpdate</c> GameMessage (opcode <c>0xF74E</c>). The
/// server broadcasts this when a remote entity's velocity / omega changes
/// without an accompanying full UpdatePosition — most importantly when a
/// remote player JUMPS. Without handling this, remote jumps look like
/// the player teleported through a tiny vertical hop and back: we never
/// see the +Z velocity that would integrate into a proper arc.
///
/// <para>
/// Wire layout (see
/// <c>references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageVectorUpdate.cs</c>):
/// </para>
/// <list type="bullet">
/// <item><b>u32 opcode</b> — 0xF74E</item>
/// <item><b>u32 objectGuid</b></item>
/// <item><b>3xf32 velocity</b> — world-space (already rotated by ACE's
/// GameMessageVectorUpdate.cs:20 from PhysicsObj.Velocity).</item>
/// <item><b>3xf32 omega</b> — world-space angular velocity.</item>
/// <item><b>u16 instanceSequence</b> — for stale-packet rejection.</item>
/// <item><b>u16 vectorSequence</b> — for stale-packet rejection.</item>
/// </list>
///
/// Total body size after opcode: 32 bytes.
/// </summary>
public static class VectorUpdate
{
public const uint Opcode = 0xF74Eu;
public readonly record struct Parsed(
uint Guid,
Vector3 Velocity,
Vector3 Omega,
ushort InstanceSequence,
ushort VectorSequence);
/// <summary>
/// Parse a 0xF74E body. <paramref name="body"/> must start with the
/// 4-byte opcode (matches the convention used by UpdateMotion /
/// UpdatePosition / etc.). Returns null on truncation or opcode
/// mismatch.
/// </summary>
public static Parsed? TryParse(ReadOnlySpan<byte> body)
{
// K-fix16 (2026-04-26): body convention includes the opcode at
// offset 0 — every other parser in this folder verifies the
// opcode word before reading payload fields. The previous
// version of this method skipped that, reading guid from the
// opcode bytes (and shifting every subsequent field by 4),
// which is why VU.recv lines showed guid=0xF74E and gigantic
// garbage velocity values.
if (body.Length < 4 + 32) return null;
try
{
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(0, 4));
if (opcode != Opcode) return null;
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4));
float vx = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(8, 4)));
float vy = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(12, 4)));
float vz = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(16, 4)));
float ox = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(20, 4)));
float oy = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(24, 4)));
float oz = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(28, 4)));
ushort instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(32, 2));
ushort vecSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(34, 2));
return new Parsed(guid, new Vector3(vx, vy, vz), new Vector3(ox, oy, oz), instSeq, vecSeq);
}
catch
{
return null;
}
}
}