using System;
using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
///
/// Inbound skill update GameMessage for the local player
/// (0x02DD) — the server's authoritative answer to RaiseSkill /
/// TrainSkill / specialize / untrain / reset. A standalone GameMessage
/// like , NOT a 0xF7B0 GameEvent.
///
///
/// Campaign CA slice CA2 (#431). NOTE the opcode-neighborhood trap this
/// slice also corrected: 0x02DD is PrivateUpdateSkill, not
/// PrivateUpdatePropertyString (which is 0x02D5) — ACE
/// GameMessageOpcode.cs:21,29. The related ranks-only
/// PrivateUpdateSkillLevel (0x02DF) has NO producer anywhere in
/// ACE (verified 2026-08-24) and is deliberately not parsed.
///
///
///
/// Wire layout — three-source agreement (ACE
/// GameMessagePrivateUpdateSkill.cs:8-24; Chorizite
/// Skill.generated.cs:22-86; holtburger
/// player/types.rs:71-131 with the golden fixture at
/// :279-293 confirming adjustPP=1 on real captures), full
/// citations in
/// docs/research/2026-08-24-advancement-wire-and-recompute.md §2.8:
///
///
/// PrivateUpdateSkill (0x02DD):
/// u32 opcode = 0x02DD
/// u8 sequence // ByteSequence, per-skill counter
/// u32 skillId // Skill enum ordinal
/// u16 ranks // LevelFromPP — ushort on the wire!
/// u16 adjustPP // hardcoded 1 by ACE on every send
/// u32 advancementClass // SkillAdvancementClass (1=Untrained/2=Trained/3=Specialized)
/// u32 xp // ExperienceSpent / PP
/// u32 init // InitLevel
/// u32 resistance // ResistanceAtLastCheck
/// f64 lastUsedTime
///
///
public static class PrivateUpdateSkill
{
public const uint Opcode = 0x02DDu;
/// Parsed skill update. Ranks widened from the wire's u16.
public readonly record struct Parsed(
byte Sequence,
uint SkillId,
uint Ranks,
ushort AdjustPP,
uint AdvancementClass,
uint Xp,
uint Init,
uint Resistance,
double LastUsed);
///
/// Parse a raw PrivateUpdateSkill (0x02DD) body. Returns
/// null on opcode mismatch or truncation.
///
public static Parsed? TryParse(ReadOnlySpan body)
{
// 4 (opcode) + 1 (seq) + 4 + 2 + 2 + 4*4 + 8 = 37 bytes minimum.
if (body.Length < 37) return null;
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
if (opcode != Opcode) return null;
int pos = 4;
byte seq = body[pos]; pos += 1;
uint skillId = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
ushort ranks = BinaryPrimitives.ReadUInt16LittleEndian(body[pos..]); pos += 2;
ushort adjustPP = BinaryPrimitives.ReadUInt16LittleEndian(body[pos..]); pos += 2;
uint sac = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
uint xp = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
uint init = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
uint resistance = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
double lastUsed = BitConverter.Int64BitsToDouble(
BinaryPrimitives.ReadInt64LittleEndian(body[pos..]));
return new Parsed(
seq, skillId, ranks, adjustPP, sac, xp, init, resistance, lastUsed);
}
}