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