using System; using System.Buffers.Binary; namespace AcDream.Core.Net.Messages; /// /// Inbound vital-update GameMessages for the local player. /// These do NOT ride the 0xF7B0 GameEvent envelope — they're /// standalone GameMessages dispatched the same way as /// / CreateObject / UpdateMotion. /// /// /// Wire format reference: holtburger /// crates/holtburger-protocol/src/messages/player/types.rs /// UpdateVital + UpdateVitalCurrent. ACE producers: /// GameMessagePrivateUpdateVital + /// GameMessagePrivateUpdateAttribute2ndLevel. The "sequence" /// field is a single byte per ACE /// ByteSequence.NextBytes; not 4 bytes despite some ACE writer /// signatures looking like uint. /// /// /// Wire layouts (private flavour, no object_guid): /// /// PrivateUpdateVital (0x02E7): /// u32 opcode = 0x02E7 /// u8 sequence /// u32 vital // ACE Vital enum (1=MaxHealth..6=Mana) /// u32 ranks /// u32 start // StartingValue /// u32 xp // ExperienceSpent /// u32 current /// /// /// PrivateUpdateVitalCurrent (0x02E9) — current-only delta (regen / drain): /// u32 opcode = 0x02E9 /// u8 sequence /// u32 vital /// u32 current /// /// /// /// Vital ID semantics live in /// . /// /// public static class PrivateUpdateVital { public const uint FullOpcode = 0x02E7u; public const uint CurrentOpcode = 0x02E9u; /// Parsed full-update message. public readonly record struct ParsedFull( byte Sequence, uint VitalId, uint Ranks, uint Start, uint Xp, uint Current); /// Parsed current-only delta. public readonly record struct ParsedCurrent( byte Sequence, uint VitalId, uint Current); /// /// Parse a raw PrivateUpdateVital (0x02E7) body. Returns /// null if opcode mismatch or truncated. /// public static ParsedFull? TryParseFull(ReadOnlySpan body) { // 4 (opcode) + 1 (seq) + 5 * 4 (uints) = 25 bytes minimum. if (body.Length < 25) return null; uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body); if (opcode != FullOpcode) return null; int pos = 4; byte seq = body[pos]; pos += 1; uint vital = 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..]); pos += 4; uint current = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); return new ParsedFull(seq, vital, ranks, start, xp, current); } /// /// Parse a raw PrivateUpdateVitalCurrent (0x02E9) body. Returns /// null if opcode mismatch or truncated. /// public static ParsedCurrent? TryParseCurrent(ReadOnlySpan body) { // 4 (opcode) + 1 (seq) + 2 * 4 (uints) = 13 bytes minimum. if (body.Length < 13) return null; uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body); if (opcode != CurrentOpcode) return null; int pos = 4; byte seq = body[pos]; pos += 1; uint vital = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4; uint current = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); return new ParsedCurrent(seq, vital, current); } }