The golden-byte tests we had proved that a parser agreed with whoever typed the hex literal. That is a weaker claim than it looks: if the author misread the oracle, the test cements the misreading. This adds AceWireWriter, a line-for-line mirror of ACE's Extensions.cs writers, so a fixture is produced by the same algorithm the authoritative server uses. Each primitive cites the ACE line it ports, including the string16L padding rule whose comment in ACE reads "client expects string length to be a multiple of 4 including the 2 bytes for length". On top of that harness, two inbound families get field-exact coverage they had none of. VectorUpdate (0xF74E) is driven in GameMessageVectorUpdate.cs's write order and pinned at ACE's declared 36-byte length, with cases for the remote-jump +Z velocity, planar velocity plus yaw omega, rest, and all-negative components so a sign or field-order slip cannot pass. The two script-playback messages follow GameMessageScript.cs: PlayScriptId (0xF754) as guid plus script DID, and PlayEffect (0xF755) as guid, type, and a free intensity float. The NaN case documents the parser's deliberate choice to retain non-finite intensities for the resolver to reject rather than coercing them at parse time, which is behavior worth locking down. Core.Net tests go 600 to 617, all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.8 KiB
C#
101 lines
3.8 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Golden-byte conformance for inbound <c>VectorUpdate (0xF74E)</c>.
|
|
///
|
|
/// <para><b>Oracle derivation.</b> Bytes are generated by
|
|
/// <see cref="AceWireWriter"/>, a faithful mirror of ACE's
|
|
/// <c>Extensions.cs</c> writers, driven in the exact order that
|
|
/// <c>ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageVectorUpdate.cs</c>
|
|
/// writes them:</para>
|
|
/// <code>
|
|
/// 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
|
|
/// </code>
|
|
/// <para>ACE declares the message length as 36 bytes (4 opcode + 32 payload),
|
|
/// which matches <see cref="VectorUpdate"/>'s <c>4 + 32</c> guard.</para>
|
|
/// <para>Cross-checked against holtburger's client-side reader for the same
|
|
/// opcode (<c>crates/holtburger-protocol/src/messages/</c>).</para>
|
|
/// </summary>
|
|
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<string, uint, Vector3, Vector3, ushort, ushort> 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)));
|
|
}
|
|
}
|