using System; using System.Numerics; using AcDream.Core.Net.Messages; using Xunit; namespace AcDream.Core.Net.Tests.Messages; /// /// Golden-byte conformance for inbound VectorUpdate (0xF74E). /// /// Oracle derivation. Bytes are generated by /// , a faithful mirror of ACE's /// Extensions.cs writers, driven in the exact order that /// ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageVectorUpdate.cs /// writes them: /// /// Writer.WriteGuid(worldObject.Guid); // u32 /// Writer.Write(velocity); // 3 x f32 /// Writer.Write(omega); // 3 x f32 /// Writer.Write(... ObjectInstance); // u16 /// Writer.Write(... ObjectVector); // u16 /// /// ACE declares the message length as 36 bytes (4 opcode + 32 payload), /// which matches 's 4 + 32 guard. /// Cross-checked against holtburger's client-side reader for the same /// opcode (crates/holtburger-protocol/src/messages/). /// public class VectorUpdateGoldenTests { private static byte[] Golden( uint guid, Vector3 velocity, Vector3 omega, ushort instanceSequence, ushort vectorSequence) => AceWireWriter.GameMessage(VectorUpdate.Opcode) .WriteGuid(guid) .Write(velocity.X).Write(velocity.Y).Write(velocity.Z) .Write(omega.X).Write(omega.Y).Write(omega.Z) .Write(instanceSequence) .Write(vectorSequence) .ToArray(); public static TheoryData Cases() => new() { // A remote player jumping: +Z velocity, no spin. { "jump", 0x50000001u, new Vector3(0f, 0f, 6.1f), Vector3.Zero, 355, 42 }, // Running with a heading change: planar velocity plus yaw omega. { "run+turn", 0x7C95B01Au, new Vector3(2.94f, -1.25f, 0f), new Vector3(0f, 0f, 1.5f), 1, 2 }, // Rest state — every field zero except the sequences. { "at-rest", 0x800114C0u, Vector3.Zero, Vector3.Zero, 0, 0 }, // Negative components on every axis, to pin sign handling. { "negatives", 0xA9B40001u, new Vector3(-1f, -2f, -3f), new Vector3(-4f, -5f, -6f), 65535, 65534 }, }; [Theory] [MemberData(nameof(Cases))] public void TryParse_AceGoldenBytes_DecodesEveryFieldExactly( string label, uint guid, Vector3 velocity, Vector3 omega, ushort instanceSequence, ushort vectorSequence) { byte[] body = Golden(guid, velocity, omega, instanceSequence, vectorSequence); // ACE's GameMessageVectorUpdate declares length 36 (opcode + payload). Assert.Equal(36, body.Length); VectorUpdate.Parsed? parsed = VectorUpdate.TryParse(body); Assert.NotNull(parsed); Assert.Equal(guid, parsed!.Value.Guid); Assert.Equal(velocity, parsed.Value.Velocity); Assert.Equal(omega, parsed.Value.Omega); Assert.Equal(instanceSequence, parsed.Value.InstanceSequence); Assert.Equal(vectorSequence, parsed.Value.VectorSequence); Assert.False(string.IsNullOrEmpty(label)); } [Fact] public void TryParse_WrongOpcode_ReturnsNull() { byte[] body = AceWireWriter.GameMessage(0xF74Cu) .WriteGuid(1u) .Write(0f).Write(0f).Write(0f) .Write(0f).Write(0f).Write(0f) .Write((ushort)0).Write((ushort)0) .ToArray(); Assert.Null(VectorUpdate.TryParse(body)); } [Fact] public void TryParse_TruncatedByOneByte_ReturnsNull() { byte[] body = Golden(1u, Vector3.One, Vector3.One, 1, 1); Assert.Null(VectorUpdate.TryParse(body.AsSpan(0, body.Length - 1))); } }