using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
///
/// Inbound ObjDescEvent GameMessage (opcode 0xF625). ACE
/// broadcasts this whenever a creature/player's appearance changes after
/// the initial spawn — equip / unequip
/// (Creature_Equipment.cs:365), tailoring (Tailoring.cs:504), recipe
/// results (RecipeManager.cs:403), character-option toggles. Skunkwors
/// protocol docs: "F625: Change Model — Sent whenever a character changes
/// their clothes. It contains the entire description of what they're
/// wearing (and possibly their facial features as well). This message is
/// only sent for changes; when the character is first created, the body
/// of this message is included inside the creation message."
///
/// Retail handles it via SmartBox::HandleObjDescEvent
/// (named-retail symbol 0x453340). acdream silently dropped it through
/// 2026-05-06 — the bug was that retail-driven characters observed from
/// acdream rendered with the wrong skin/hair palettes because the
/// follow-up appearance updates were never applied.
///
/// Wire layout (ACE WorldObject_Networking.cs:48-54
/// SerializeUpdateModelData):
///
/// - u32 opcode (0xF625)
/// - u32 guid — target object
/// - ModelData block — see
/// - u32 instanceSequence
/// - u32 visualDescSequence
///
///
public static class ObjDescEvent
{
public const uint Opcode = 0xF625u;
///
/// One ObjDescEvent: target guid + the new ModelData. Sequence
/// counters are read but not surfaced (subscribers don't need them
/// — the event always carries the full new appearance).
///
public readonly record struct Parsed(uint Guid, CreateObject.ModelData ModelData);
///
/// Parse an ObjDescEvent body (must start with the 4-byte opcode).
/// Returns null on truncation or wrong opcode.
///
public static Parsed? TryParse(ReadOnlySpan body)
{
try
{
int pos = 0;
if (body.Length - pos < 4) return null;
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
if (opcode != Opcode) return null;
if (body.Length - pos < 4) return null;
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
var modelData = CreateObject.ReadModelData(body, ref pos);
// Trailing instanceSeq + visualDescSeq are read for completeness
// but not surfaced — subscribers re-render unconditionally on
// every event since each carries the full appearance.
return new Parsed(guid, modelData);
}
catch
{
return null;
}
}
}