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
/// - u16 instanceSequence
/// - u16 visualDescSequence (OBJDESC_TS)
///
///
public static class ObjDescEvent
{
public const uint Opcode = 0xF625u;
///
/// One ObjDescEvent: target guid, new ModelData, and retail's packed
/// INSTANCE_TS/OBJDESC_TS pair.
///
public readonly record struct Parsed(
uint Guid,
CreateObject.ModelData ModelData,
ushort InstanceSequence,
ushort ObjDescSequence);
///
/// 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);
// PhysicsTimestampPack::UnPack 0x00516F50 is exactly two u16s.
// Reject a missing or overlong tail so cursor mistakes cannot turn
// into an apparently valid, ungated appearance update.
if (body.Length - pos != 4) return null;
ushort instance = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
ushort objDesc = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 2));
return new Parsed(guid, modelData, instance, objDesc);
}
catch
{
return null;
}
}
}