using System; using System.Buffers.Binary; namespace AcDream.Core.Net.Messages; /// /// Inbound primary-attribute update GameMessage for the local player /// (0x02E3) โ€” the server's authoritative answer to a RaiseAttribute /// action (and any other server-side attribute change). A standalone /// GameMessage like , NOT a 0xF7B0 /// GameEvent. /// /// /// Campaign CA slice CA2 (#431): until this parser existed the client's /// attribute model was stale from login's PlayerDescription until the next /// login โ€” every post-raise derived value (skills, vitals maxima, run /// rate) computed from old attributes. /// /// /// /// Wire layout โ€” three-source agreement (ACE /// GameMessagePrivateUpdateAttribute.cs:8-16; Chorizite /// AttributeInfo.generated.cs:26-54; holtburger /// player/types.rs:22-53 UpdateAttribute<false>), full /// citations in /// docs/research/2026-08-24-advancement-wire-and-recompute.md ยง2.5: /// /// /// PrivateUpdateAttribute (0x02E3): /// u32 opcode = 0x02E3 /// u8 sequence // ByteSequence, per-attribute counter /// u32 attribute // PropertyAttribute (1=Strength..6=Self) /// u32 ranks /// u32 start // StartingValue / InitLevel /// u32 xp // ExperienceSpent / CPSpent /// /// public static class PrivateUpdateAttribute { public const uint Opcode = 0x02E3u; /// Parsed attribute update. public readonly record struct Parsed( byte Sequence, uint AttributeId, uint Ranks, uint Start, uint Xp); /// /// Parse a raw PrivateUpdateAttribute (0x02E3) body. Returns /// null on opcode mismatch or truncation. /// public static Parsed? TryParse(ReadOnlySpan body) { // 4 (opcode) + 1 (seq) + 4 * 4 (uints) = 21 bytes minimum. if (body.Length < 21) return null; uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body); if (opcode != Opcode) return null; int pos = 4; byte seq = body[pos]; pos += 1; uint attr = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; uint ranks = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; uint start = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; uint xp = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); return new Parsed(seq, attr, ranks, start, xp); } }