using System.Buffers.Binary;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Tests.Messages;
public sealed class ObjDescEventTests
{
[Fact]
public void TryParse_RejectsWrongOpcode()
{
byte[] body = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(body, 0xF745u); // CreateObject opcode
Assert.Null(ObjDescEvent.TryParse(body));
}
[Fact]
public void TryParse_RejectsTruncatedBody()
{
Assert.Null(ObjDescEvent.TryParse(new byte[3]));
}
///
/// Round-trip a synthesized body: opcode + guid + ModelData (3 SubPalettes,
/// 4 TextureChanges, 0 AnimPartChanges — same shape as the captured retail
/// 152-byte +Je ObjDescEvent body) + trailing sequence pair. Verifies the
/// parser surfaces the same fields the writer wrote.
///
[Fact]
public void TryParse_SynthesizedBody_ExtractsGuidAndModelData()
{
// Build a body matching the wire shape we see from ACE.
var bytes = new List();
AppendU32(bytes, ObjDescEvent.Opcode);
AppendU32(bytes, 0x50000001u); // target guid
// ModelData header: marker, subPalCount, texCount, animPartCount.
bytes.Add(0x11);
bytes.Add(3); // subPalCount
bytes.Add(4); // texChangeCount
bytes.Add(0); // animPartCount
// BasePaletteId (palette type prefix stripped before packing).
AppendPackedDword(bytes, 0x0400007Eu, 0x04000000u);
// SubPalettes — three skin/hair-style overlays at varied offsets.
AppendPackedDword(bytes, 0x04001FE3u, 0x04000000u);
bytes.Add(24); bytes.Add(8);
AppendPackedDword(bytes, 0x040002BAu, 0x04000000u);
bytes.Add(0); bytes.Add(24);
AppendPackedDword(bytes, 0x040002BCu, 0x04000000u);
bytes.Add(32); bytes.Add(8);
// TextureChanges — four part textures.
for (byte partIdx = 0; partIdx < 4; partIdx++)
{
bytes.Add(partIdx);
AppendPackedDword(bytes, 0x05000100u + partIdx, 0x05000000u);
AppendPackedDword(bytes, 0x05000200u + partIdx, 0x05000000u);
}
// 4-byte align after AnimPartChanges (none here, so just align).
while (bytes.Count % 4 != 0) bytes.Add(0);
// Trailing instance + visual-desc sequences (consumed but ignored).
AppendU32(bytes, 0x12345678u);
AppendU32(bytes, 0x9ABCDEF0u);
var parsed = ObjDescEvent.TryParse(bytes.ToArray());
Assert.NotNull(parsed);
Assert.Equal(0x50000001u, parsed!.Value.Guid);
var md = parsed.Value.ModelData;
Assert.Equal(0x0400007Eu, md.BasePaletteId);
Assert.Equal(3, md.SubPalettes.Count);
Assert.Equal(0x04001FE3u, md.SubPalettes[0].SubPaletteId);
Assert.Equal(24, md.SubPalettes[0].Offset);
Assert.Equal(8, md.SubPalettes[0].Length);
Assert.Equal(0x040002BAu, md.SubPalettes[1].SubPaletteId);
Assert.Equal(0, md.SubPalettes[1].Offset);
Assert.Equal(24, md.SubPalettes[1].Length);
Assert.Equal(4, md.TextureChanges.Count);
Assert.Equal(0, md.TextureChanges[0].PartIndex);
Assert.Equal(0x05000100u, md.TextureChanges[0].OldTexture);
Assert.Equal(0x05000200u, md.TextureChanges[0].NewTexture);
Assert.Equal(3, md.TextureChanges[3].PartIndex);
Assert.Empty(md.AnimPartChanges);
}
///
/// Confirms ReadModelData (the shared helper) round-trips identically
/// when called from CreateObject and from ObjDescEvent — same bytes,
/// same parsed output. Guards against the two callers drifting.
///
[Fact]
public void ReadModelData_SameOutputFromBothCallers()
{
// Bare ModelData block — used as a substring in both messages.
var modelDataBytes = new List();
modelDataBytes.Add(0x11);
modelDataBytes.Add(1); // subPalCount
modelDataBytes.Add(0); // texCount
modelDataBytes.Add(0); // animPartCount
AppendPackedDword(modelDataBytes, 0x0400007Eu, 0x04000000u);
AppendPackedDword(modelDataBytes, 0x04001084u, 0x04000000u);
modelDataBytes.Add(80); modelDataBytes.Add(12);
while (modelDataBytes.Count % 4 != 0) modelDataBytes.Add(0);
ReadOnlySpan span = modelDataBytes.ToArray();
int pos = 0;
var md = CreateObject.ReadModelData(span, ref pos);
Assert.Equal(0x0400007Eu, md.BasePaletteId);
Assert.Single(md.SubPalettes);
Assert.Equal(0x04001084u, md.SubPalettes[0].SubPaletteId);
Assert.Equal(80, md.SubPalettes[0].Offset);
Assert.Equal(12, md.SubPalettes[0].Length);
}
private static void AppendU32(List dest, uint value)
{
Span tmp = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(tmp, value);
dest.AddRange(tmp.ToArray());
}
///
/// Mirror of ACE's WritePackedDwordOfKnownType: strip the type prefix
/// if it matches , then write as a 16- or
/// 32-bit packed value.
///
private static void AppendPackedDword(List dest, uint value, uint knownType)
{
uint packed = (value & 0xFF000000u) == knownType ? (value & ~knownType) : value;
if (packed <= 0x7FFFu)
{
Span tmp = stackalloc byte[2];
BinaryPrimitives.WriteUInt16LittleEndian(tmp, (ushort)packed);
dest.AddRange(tmp.ToArray());
}
else
{
ushort high = (ushort)((packed >> 16) | 0x8000);
ushort low = (ushort)(packed & 0xFFFFu);
Span tmp = stackalloc byte[4];
BinaryPrimitives.WriteUInt16LittleEndian(tmp, high);
BinaryPrimitives.WriteUInt16LittleEndian(tmp.Slice(2), low);
dest.AddRange(tmp.ToArray());
}
}
}