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()); } } }