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