using System; using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Messages; /// /// Parser + record types for the most-used /// sub-opcodes inside the 0xF7B0 envelope. Each parser takes the /// slice (header stripped) and /// returns a typed record or null on malformed payload. /// /// /// References: r08 protocol atlas §4 (wire specs) + ACE /// GameEventChat.cs, GameEventTell.cs, /// GameEventUpdateHealth.cs, GameEventWeenieError.cs, /// GameEventCommunicationTransientString.cs. /// /// public static class GameEvents { // ── Chat / communication ───────────────────────────────────────────────── /// 0x0147 ChannelBroadcast payload. public readonly record struct ChannelBroadcast( uint ChannelId, string SenderName, string Message); public static ChannelBroadcast? ParseChannelBroadcast(ReadOnlySpan payload) { int pos = 0; if (payload.Length < 4) return null; uint channelId = BinaryPrimitives.ReadUInt32LittleEndian(payload); pos += 4; try { string sender = ReadString16L(payload, ref pos); string message = ReadString16L(payload, ref pos); return new ChannelBroadcast(channelId, sender, message); } catch { return null; } } /// 0x02BD Tell payload. public readonly record struct Tell( string Message, string SenderName, uint SenderGuid, uint TargetGuid, uint ChatType); public static Tell? ParseTell(ReadOnlySpan payload) { int pos = 0; try { string message = ReadString16L(payload, ref pos); string sender = ReadString16L(payload, ref pos); if (payload.Length - pos < 12) return null; uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint targetGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; return new Tell(message, sender, senderGuid, targetGuid, chatType); } catch { return null; } } /// 0x02EB CommunicationTransientString payload. public readonly record struct TransientMessage(string Message, uint ChatType); public static TransientMessage? ParseTransient(ReadOnlySpan payload) { int pos = 0; try { string message = ReadString16L(payload, ref pos); if (payload.Length - pos < 4) return null; uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); return new TransientMessage(message, chatType); } catch { return null; } } /// 0x0004 PopupString — modal dialog text. public static string? ParsePopupString(ReadOnlySpan payload) { int pos = 0; try { return ReadString16L(payload, ref pos); } catch { return null; } } // ── Errors ────────────────────────────────────────────────────────────── /// 0x028A WeenieError: generic game-logic failure code. public static uint? ParseWeenieError(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x028B WeenieErrorWithString. public readonly record struct WeenieErrorWithString(uint ErrorCode, string Interpolation); public static WeenieErrorWithString? ParseWeenieErrorWithString(ReadOnlySpan payload) { if (payload.Length < 4) return null; uint code = BinaryPrimitives.ReadUInt32LittleEndian(payload); int pos = 4; try { string interp = ReadString16L(payload, ref pos); return new WeenieErrorWithString(code, interp); } catch { return null; } } // ── Vitals / combat ───────────────────────────────────────────────────── /// 0x01C0 UpdateHealth: (guid, healthPercent 0..1). public readonly record struct UpdateHealth(uint TargetGuid, float HealthPercent); public static UpdateHealth? ParseUpdateHealth(ReadOnlySpan payload) { if (payload.Length < 8) return null; uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload); float pct = BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(4)); return new UpdateHealth(guid, pct); } // ── Pings / misc ──────────────────────────────────────────────────────── /// 0x01EA PingResponse: echoes the client ping id. public static uint? ParsePingResponse(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } // ── Spells / magic ────────────────────────────────────────────────────── /// 0x02C1 MagicUpdateSpell: spell id added to spellbook. public static uint? ParseMagicUpdateSpell(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } // ── Combat notifications ──────────────────────────────────────────────── /// 0x01AC VictimNotification - death message for the victim. public readonly record struct VictimNotification(string DeathMessage); public static VictimNotification? ParseVictimNotification(ReadOnlySpan payload) { int pos = 0; try { return new VictimNotification(ReadString16L(payload, ref pos)); } catch { return null; } } /// 0x01AD KillerNotification - death message for the killer. public readonly record struct KillerNotification(string DeathMessage); public static KillerNotification? ParseKillerNotification(ReadOnlySpan payload) { int pos = 0; try { return new KillerNotification(ReadString16L(payload, ref pos)); } catch { return null; } } /// 0x01B1 AttackerNotification - "you hit X". public readonly record struct AttackerNotification( string DefenderName, uint DamageType, double HealthPercent, uint Damage, uint Critical, ulong AttackConditions); public static AttackerNotification? ParseAttackerNotification(ReadOnlySpan payload) { int pos = 0; try { string name = ReadString16L(payload, ref pos); if (payload.Length - pos < 28) return null; uint damageType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; double pct = BinaryPrimitives.ReadDoubleLittleEndian(payload.Slice(pos)); pos += 8; uint damage = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; ulong cond = BinaryPrimitives.ReadUInt64LittleEndian(payload.Slice(pos)); pos += 8; return new AttackerNotification(name, damageType, pct, damage, crit, cond); } catch { return null; } } /// 0x01B2 DefenderNotification - "X hit you". public readonly record struct DefenderNotification( string AttackerName, uint DamageType, double HealthPercent, uint Damage, uint HitQuadrant, uint Critical, ulong AttackConditions); public static DefenderNotification? ParseDefenderNotification(ReadOnlySpan payload) { int pos = 0; try { string name = ReadString16L(payload, ref pos); if (payload.Length - pos < 32) return null; uint dtype = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; double pct = BinaryPrimitives.ReadDoubleLittleEndian(payload.Slice(pos)); pos += 8; uint dmg = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint quad = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; ulong cond = BinaryPrimitives.ReadUInt64LittleEndian(payload.Slice(pos)); pos += 8; return new DefenderNotification(name, dtype, pct, dmg, quad, crit, cond); } catch { return null; } } /// 0x01B3 EvasionAttackerNotification - "X evaded". public static string? ParseEvasionAttackerNotification(ReadOnlySpan payload) { int pos = 0; try { return ReadString16L(payload, ref pos); } catch { return null; } } /// 0x01B4 EvasionDefenderNotification - "you evaded X". public static string? ParseEvasionDefenderNotification(ReadOnlySpan payload) { int pos = 0; try { return ReadString16L(payload, ref pos); } catch { return null; } } /// 0x01B8 CombatCommenceAttack - empty payload. public static bool ParseCombatCommenceAttack(ReadOnlySpan payload) => payload.Length == 0; /// 0x01A7 AttackDone - single WeenieError value. public readonly record struct AttackDone(uint AttackSequence, uint WeenieError); public static AttackDone? ParseAttackDone(ReadOnlySpan payload) { if (payload.Length < 4) return null; return new AttackDone(0u, BinaryPrimitives.ReadUInt32LittleEndian(payload)); } // ── Spell enchantments ────────────────────────────────────────────────── /// /// 0x02C3 MagicRemoveEnchantment — (layerId, spellId). /// public readonly record struct MagicRemoveEnchantment(uint LayerId, uint SpellId); public static MagicRemoveEnchantment? ParseMagicRemoveEnchantment(ReadOnlySpan payload) { if (payload.Length < 8) return null; return new MagicRemoveEnchantment( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4))); } /// 0x01A8 MagicRemoveSpell — spell id removed from spellbook. public static uint? ParseMagicRemoveSpell(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// /// 0x02C2 MagicUpdateEnchantment — the Enchantment blob. Full layout /// (ACE Enchantment.Pack) is ~80+ bytes of spell metadata + /// stat mods. We expose the first few fields that drive the enchant /// bar UI; the rest is available via the raw payload view. /// public readonly record struct EnchantmentSummary( uint SpellId, uint LayerId, float Duration, uint CasterGuid); public static EnchantmentSummary? ParseMagicUpdateEnchantment(ReadOnlySpan payload) { // Layout (ACE Enchantment.Pack): // u32 spellId // u32 layerId // f32 duration // u32 casterGuid // ... (stat mods, category, power, etc.) if (payload.Length < 16) return null; return new EnchantmentSummary( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)), BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(8)), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(12))); } /// /// 0x02C7 MagicDispelEnchantment — (layerId, spellId). /// Structure matches MagicRemoveEnchantment. /// public static MagicRemoveEnchantment? ParseMagicDispelEnchantment(ReadOnlySpan payload) => ParseMagicRemoveEnchantment(payload); // ── Appraise / identify ───────────────────────────────────────────────── /// 0x00C9 IdentifyObjectResponse header. public readonly record struct IdentifyResponseHeader( uint Guid, uint AppraiseFlags, bool Success); /// /// Parse the header of an IdentifyObjectResponse (0x00C9). /// Full property-bundle deserialization (int / bool / float / string /// tables per the AppraiseFlags bitfield) is a future pass; this /// header alone is enough for the UI to display "Appraise complete /// on target X" and to route into the repository. /// public static IdentifyResponseHeader? ParseIdentifyResponseHeader(ReadOnlySpan payload) { if (payload.Length < 12) return null; uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload); uint flags = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)); uint success = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)); return new IdentifyResponseHeader(guid, flags, success != 0); } /// 0x0023 WieldObject: server-driven equip. public readonly record struct WieldObject( uint ItemGuid, uint EquipLoc, uint WielderGuid); public static WieldObject? ParseWieldObject(ReadOnlySpan payload) { if (payload.Length < 12) return null; return new WieldObject( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8))); } /// 0x0022 InventoryPutObjInContainer: server puts item into container slot. public readonly record struct InventoryPutObjInContainer( uint ItemGuid, uint ContainerGuid, uint Placement); public static InventoryPutObjInContainer? ParsePutObjInContainer(ReadOnlySpan payload) { if (payload.Length < 12) return null; return new InventoryPutObjInContainer( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8))); } // ── Other small-payload events ────────────────────────────────────────── /// 0x01C7 UseDone: the Use/UseWithTarget completion signal (WeenieError code). public static uint? ParseUseDone(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x019A InventoryPutObjectIn3D: server dropped item to ground. public static uint? ParsePutObjectIn3D(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x00A0 InventoryServerSaveFailed: revert a speculative local inventory op. public static uint? ParseInventoryServerSaveFailed(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x0052 CloseGroundContainer: server closed a ground container view. public static uint? ParseCloseGroundContainer(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x0207 TradeFailure: server trade error code. public static uint? ParseTradeFailure(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x0200 AddToTrade: (itemGuid, slotIndex). public readonly record struct AddToTrade(uint ItemGuid, uint SlotIndex); public static AddToTrade? ParseAddToTrade(ReadOnlySpan payload) { if (payload.Length < 8) return null; return new AddToTrade( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4))); } /// 0x0202 AcceptTrade: initiator guid. public static uint? ParseAcceptTrade(ReadOnlySpan payload) { if (payload.Length < 4) return null; return BinaryPrimitives.ReadUInt32LittleEndian(payload); } /// 0x0264 QueryItemManaResponse: (itemGuid, manaPercent). public readonly record struct QueryItemManaResponse(uint ItemGuid, float ManaPercent); public static QueryItemManaResponse? ParseQueryItemManaResponse(ReadOnlySpan payload) { if (payload.Length < 8) return null; return new QueryItemManaResponse( BinaryPrimitives.ReadUInt32LittleEndian(payload), BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(4))); } /// 0x0274 CharacterConfirmationRequest — server-driven modal confirm. public readonly record struct CharacterConfirmationRequest( uint Type, uint ContextId, uint OtherGuid, string Message); public static CharacterConfirmationRequest? ParseCharacterConfirmationRequest(ReadOnlySpan payload) { if (payload.Length < 12) return null; int pos = 0; uint type = BinaryPrimitives.ReadUInt32LittleEndian(payload); pos += 4; uint contextId = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; uint otherGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4; try { string msg = ReadString16L(payload, ref pos); return new CharacterConfirmationRequest(type, contextId, otherGuid, msg); } catch { return null; } } // ── Shared string reader (matches LoginRequest.ReadString16L) ─────────── private static string ReadString16L(ReadOnlySpan source, ref int pos) { if (source.Length - pos < 2) throw new FormatException("truncated String16L length"); ushort length = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos)); pos += 2; if (source.Length - pos < length) throw new FormatException("truncated String16L body"); // Windows-1252 matches retail (and holtburger's encoding_rs::WINDOWS_1252). string result = Encoding.GetEncoding(1252).GetString(source.Slice(pos, length)); pos += length; int recordSize = 2 + length; int padding = (4 - (recordSize & 3)) & 3; pos += padding; return result; } }