using System.Buffers.Binary; using System.Text; using AcDream.Core.Net.Messages; namespace AcDream.Core.Net.Tests; /// /// Wire-format tests for . /// Builds synthetic payloads matching ACE /// GameEventPlayerDescription.WriteEventBody and confirms the /// walker extracts the attribute block + early sections correctly. /// public sealed class PlayerDescriptionParserTests { /// /// Build a minimal PlayerDescription payload with empty property /// flags + no-attribute vector flags. Just header bytes — useful /// for testing the most basic walk. /// private static byte[] BuildEmpty(uint weenieType = 1u) { // u32 propertyFlags + u32 weenieType + u32 vectorFlags + u32 has_health byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, 0u); // no property flags BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), weenieType); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), 0u); // no vector flags BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), 0u); // has_health=false return body; } /// /// Build a payload with an Attribute-block-only body /// (vector_flags = ATTRIBUTE) populating all 9 entries with known /// ranks/start/xp values — primary attrs 1..6 and vitals 7..9. /// private static byte[] BuildWithFullAttributeBlock( uint healthCurrent, uint stamCurrent, uint manaCurrent) { // No property tables, no positions. // Header (8) + vectorFlags+has_health (8) + attribFlags (4) // + 6 primary entries × 12 + 3 vital entries × 16 = 8+8+4+72+48 = 140. byte[] body = new byte[140]; int p = 0; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // propertyFlags = 0 BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 1u); p += 4; // weenieType BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x01u); p += 4; // vectorFlags = ATTRIBUTE BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 1u); p += 4; // has_health = true // attributeFlags = AttributeCache.Full = 0x1FF (bits 0..8) BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x1FFu); p += 4; // Primary attrs 1..6 — ranks/start/xp triplets. for (uint i = 1; i <= 6; i++) { BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 10u * i); p += 4; // ranks BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 50u + i); p += 4; // start BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 1000u); p += 4; // xp } // Vitals 7..9 — Health (id=7), Stamina (id=8), Mana (id=9). // ranks=20, start=80 → MaxApprox = 100. Currents = test args. WriteVital(body, ref p, ranks: 20u, start: 80u, xp: 5000u, current: healthCurrent); WriteVital(body, ref p, ranks: 20u, start: 80u, xp: 5000u, current: stamCurrent); WriteVital(body, ref p, ranks: 20u, start: 80u, xp: 5000u, current: manaCurrent); return body; } private static void WriteVital(byte[] body, ref int p, uint ranks, uint start, uint xp, uint current) { BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), ranks); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), start); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), xp); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), current); p += 4; } [Fact] public void TryParse_ReturnsNull_OnTruncatedHeader() { byte[] tooShort = new byte[4]; Assert.Null(PlayerDescriptionParser.TryParse(tooShort)); } [Fact] public void TryParse_EmptyBody_ParsesHeaderOnly() { var p = PlayerDescriptionParser.TryParse(BuildEmpty(weenieType: 0x52u)); Assert.NotNull(p); Assert.Equal(0x52u, p!.Value.WeenieType); Assert.Equal(PlayerDescriptionParser.DescriptionPropertyFlag.None, p.Value.PropertyFlags); Assert.Equal(PlayerDescriptionParser.DescriptionVectorFlag.None, p.Value.VectorFlags); Assert.False(p.Value.HasHealth); Assert.Empty(p.Value.Attributes); } [Fact] public void TryParse_AttributeBlock_PopulatesAllNineEntries() { var p = PlayerDescriptionParser.TryParse( BuildWithFullAttributeBlock(healthCurrent: 90, stamCurrent: 75, manaCurrent: 60)); Assert.NotNull(p); Assert.True(p!.Value.HasHealth); Assert.Equal(9, p.Value.Attributes.Count); // Primary attrs 1..6 have null Current. for (uint i = 1; i <= 6; i++) { var attr = p.Value.Attributes.First(a => a.AtType == i); Assert.Equal(10u * i, attr.Ranks); Assert.Equal(50u + i, attr.Start); Assert.Equal(1000u, attr.Xp); Assert.Null(attr.Current); } // Vital 7 = Health (current = 90). var health = p.Value.Attributes.First(a => a.AtType == 7); Assert.Equal(20u, health.Ranks); Assert.Equal(80u, health.Start); Assert.Equal(90u, health.Current); // Vital 8 = Stamina. var stam = p.Value.Attributes.First(a => a.AtType == 8); Assert.Equal(75u, stam.Current); // Vital 9 = Mana. var mana = p.Value.Attributes.First(a => a.AtType == 9); Assert.Equal(60u, mana.Current); } [Fact] public void TryParse_SkipsPrimaryAttribute_WhenItsAttributeFlagBitIsClear() { // attribute_flags only sets bits for Strength (id=1) and Health (id=7). // Body shape: 8 header + 8 vector header + 4 attr_flags + 12 (str) + 16 (health) = 48 byte[] body = new byte[48]; int p = 0; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 1u); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x01u); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 1u); p += 4; // bit 0 (Strength = id 1) + bit 6 (Health = id 7) = 0x41 BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x41u); p += 4; // Strength entry (12 B): BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 100u); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 60u); p += 4; BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // Health entry (16 B): WriteVital(body, ref p, ranks: 25u, start: 75u, xp: 0u, current: 99u); var parsed = PlayerDescriptionParser.TryParse(body); Assert.NotNull(parsed); Assert.Equal(2, parsed!.Value.Attributes.Count); Assert.Equal(1u, parsed.Value.Attributes[0].AtType); // Strength Assert.Equal(7u, parsed.Value.Attributes[1].AtType); // Health Assert.Equal(99u, parsed.Value.Attributes[1].Current); } [Fact] public void TryParse_PropertyTablesWalked_OffsetReachesAttributeBlock() { // PROPERTY_INT32 + PROPERTY_STRING tables present, then ATTRIBUTE // block. If the walker can't skip past the property tables it'll // read garbage from the table bytes as if it were the attribute // block — this test fails on any walking error. // PROPERTY_INT32 = 0x0001, PROPERTY_STRING = 0x0010 → flags = 0x0011 var sb = new MemoryStream(); using var writer = new BinaryWriter(sb); writer.Write(0x0011u); // propertyFlags = INT32 | STRING writer.Write(0x52u); // weenieType // INT32 table: 2 entries. writer.Write((ushort)2); writer.Write((ushort)32); // buckets (ignored) writer.Write(101u); writer.Write(42); writer.Write(102u); writer.Write(-7); // STRING table: 1 entry "Acdream" (7 chars + 2 length + 3 padding = 12). writer.Write((ushort)1); writer.Write((ushort)32); // buckets writer.Write(1u); // PropertyString.Name = 1 byte[] name = Encoding.ASCII.GetBytes("Acdream"); writer.Write((ushort)name.Length); writer.Write(name); writer.Write(new byte[(4 - ((2 + name.Length) & 3)) & 3]); // pad to 4 // vectorFlags = ATTRIBUTE, has_health = 1. writer.Write(0x01u); writer.Write(1u); // Attribute block: only Health (bit 6 = 0x40), current=88. writer.Write(0x40u); WriteVitalToWriter(writer, ranks: 30u, start: 70u, xp: 0u, current: 88u); byte[] body = sb.ToArray(); var parsed = PlayerDescriptionParser.TryParse(body); Assert.NotNull(parsed); Assert.Equal(2, parsed!.Value.Properties.Ints.Count); Assert.Equal(42, parsed.Value.Properties.Ints[101]); Assert.Equal(-7, parsed.Value.Properties.Ints[102]); Assert.Equal("Acdream", parsed.Value.Properties.Strings[1]); Assert.Single(parsed.Value.Attributes); Assert.Equal(7u, parsed.Value.Attributes[0].AtType); Assert.Equal(88u, parsed.Value.Attributes[0].Current); } private static void WriteVitalToWriter(BinaryWriter w, uint ranks, uint start, uint xp, uint current) { w.Write(ranks); w.Write(start); w.Write(xp); w.Write(current); } [Fact] public void TryParse_SpellTable_PopulatesSpellsDictionary() { // ATTRIBUTE | SPELL = 0x101. Empty attribute block (flags=0). Spell // table with two entries. var sb = new MemoryStream(); using var writer = new BinaryWriter(sb); writer.Write(0u); // propertyFlags writer.Write(0x52u); // weenieType writer.Write(0x101u); // vectorFlags = ATTRIBUTE | SPELL writer.Write(1u); // has_health writer.Write(0u); // attribute_flags = 0 → no entries writer.Write((ushort)2); // spell count writer.Write((ushort)64); // spell buckets writer.Write(1234u); writer.Write(2.0f); writer.Write(5678u); writer.Write(2.0f); var parsed = PlayerDescriptionParser.TryParse(sb.ToArray()); Assert.NotNull(parsed); Assert.Equal(2, parsed!.Value.Spells.Count); Assert.Equal(2.0f, parsed.Value.Spells[1234u]); Assert.Equal(2.0f, parsed.Value.Spells[5678u]); } }