using System; using System.Buffers.Binary; using System.IO; using AcDream.Core.Net.Messages; using Xunit; namespace AcDream.Core.Net.Tests.Messages; public sealed class AppraiseInfoParserTests { /// /// Build an AppraiseInfo payload matching ACE's wire format. Starts /// with (guid, flags, success) then per-flag tables. /// private static byte[] BuildPayload( uint guid, AppraiseInfoParser.IdentifyResponseFlags flags, bool success, (uint key, int value)[]? ints = null, (uint key, bool value)[]? bools = null, (uint key, string value)[]? strings = null, uint[]? spellBook = null) { using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms); bw.Write(guid); bw.Write((uint)flags); bw.Write((uint)(success ? 1 : 0)); if (flags.HasFlag(AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable) && ints is not null) { bw.Write((ushort)ints.Length); bw.Write((ushort)16); // numBuckets hint foreach (var (k, v) in ints) { bw.Write(k); bw.Write(v); } } if (flags.HasFlag(AppraiseInfoParser.IdentifyResponseFlags.BoolStatsTable) && bools is not null) { bw.Write((ushort)bools.Length); bw.Write((ushort)8); foreach (var (k, v) in bools) { bw.Write(k); bw.Write(v ? 1u : 0u); } } if (flags.HasFlag(AppraiseInfoParser.IdentifyResponseFlags.StringStatsTable) && strings is not null) { bw.Write((ushort)strings.Length); bw.Write((ushort)8); foreach (var (k, v) in strings) { bw.Write(k); WriteString16L(bw, v); } } if (flags.HasFlag(AppraiseInfoParser.IdentifyResponseFlags.SpellBook) && spellBook is not null) { bw.Write((uint)spellBook.Length); foreach (var sid in spellBook) bw.Write(sid); } bw.Flush(); return ms.ToArray(); } private static void WriteString16L(BinaryWriter bw, string s) { byte[] bytes = System.Text.Encoding.ASCII.GetBytes(s); bw.Write((ushort)bytes.Length); bw.Write(bytes); int record = 2 + bytes.Length; int pad = (4 - (record & 3)) & 3; for (int i = 0; i < pad; i++) bw.Write((byte)0); } [Fact] public void TryParse_GuidAndFlags_ExtractedCorrectly() { byte[] payload = BuildPayload( guid: 0xDEADBEEFu, flags: AppraiseInfoParser.IdentifyResponseFlags.None, success: true); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.Equal(0xDEADBEEFu, parsed!.Value.Guid); Assert.True(parsed.Value.Success); } [Fact] public void TryParse_IntStatsTable_PopulatesIntProperties() { byte[] payload = BuildPayload( guid: 1, flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable, success: true, ints: new[] { ((uint)1, 100), ((uint)5, -50), ((uint)9, 42) }); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.Equal(3, parsed!.Value.Properties.Ints.Count); Assert.Equal(100, parsed.Value.Properties.Ints[1]); Assert.Equal(-50, parsed.Value.Properties.Ints[5]); Assert.Equal(42, parsed.Value.Properties.Ints[9]); } [Fact] public void TryParse_BoolStatsTable_ConvertsU32ToBool() { byte[] payload = BuildPayload( guid: 1, flags: AppraiseInfoParser.IdentifyResponseFlags.BoolStatsTable, success: true, bools: new[] { ((uint)1, true), ((uint)2, false) }); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.True(parsed!.Value.Properties.Bools[1]); Assert.False(parsed.Value.Properties.Bools[2]); } [Fact] public void TryParse_StringStatsTable_ParsesPaddedStrings() { byte[] payload = BuildPayload( guid: 1, flags: AppraiseInfoParser.IdentifyResponseFlags.StringStatsTable, success: true, strings: new[] { ((uint)1, "Excalibur"), ((uint)2, "Rusty Dagger") }); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.Equal("Excalibur", parsed!.Value.Properties.Strings[1]); Assert.Equal("Rusty Dagger", parsed.Value.Properties.Strings[2]); } [Fact] public void TryParse_SpellBook_ReturnsSpellIdArray() { byte[] payload = BuildPayload( guid: 1, flags: AppraiseInfoParser.IdentifyResponseFlags.SpellBook, success: true, spellBook: new uint[] { 0x3E1, 0x3E2, 0x3E3 }); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.Equal(3, parsed!.Value.SpellBook.Length); Assert.Equal(0x3E1u, parsed.Value.SpellBook[0]); } [Fact] public void TryParse_MultipleTables_AllParsed() { var flags = AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable | AppraiseInfoParser.IdentifyResponseFlags.BoolStatsTable | AppraiseInfoParser.IdentifyResponseFlags.SpellBook; byte[] payload = BuildPayload( guid: 1, flags, success: true, ints: new[] { ((uint)1, 100) }, bools: new[] { ((uint)2, true) }, spellBook: new uint[] { 42 }); var parsed = AppraiseInfoParser.TryParse(payload); Assert.NotNull(parsed); Assert.Single(parsed!.Value.Properties.Ints); Assert.Single(parsed.Value.Properties.Bools); Assert.Single(parsed.Value.SpellBook); } [Fact] public void TryParse_Truncated_ReturnsNull() { Assert.Null(AppraiseInfoParser.TryParse(new byte[4])); } }