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