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