using System; using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Messages; /// /// Inbound 0x02BB HearSpeech + 0x02BC HearRangedSpeech /// GameMessages. Local-area / shout chat heard by the player. These /// do NOT ride the 0xF7B0 GameEvent envelope — they're standalone /// GameMessages dispatched the same way as CreateObject / UpdateMotion. /// /// /// Wire layout: /// /// u32 opcode // 0x02BB or 0x02BC /// string16L text /// string16L senderName /// u32 senderGuid /// u32 chatType /// /// /// /// /// ChatType (from ACE): /// /// 0x01 = Broadcast /// 0x02 = Combat /// 0x0B = Speech /// 0x0F = Emote /// 0x10 = Tell /// 0x11 = Syllables (spell casting) /// other values in ACE ChatMessageType.cs /// /// /// public static class HearSpeech { public const uint LocalOpcode = 0x02BBu; public const uint RangedOpcode = 0x02BCu; public readonly record struct Parsed( string Text, string SenderName, uint SenderGuid, uint ChatType, bool IsRanged); public static Parsed? TryParse(byte[] body) { if (body is null || body.Length < 16) return null; uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body); bool isRanged; if (opcode == LocalOpcode) isRanged = false; else if (opcode == RangedOpcode) isRanged = true; else return null; int pos = 4; try { string text = ReadString16L(body, ref pos); string sender = ReadString16L(body, ref pos); if (body.Length - pos < 8) return null; uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos)); pos += 4; uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos)); pos += 4; return new Parsed(text, sender, senderGuid, chatType, isRanged); } catch { return null; } } 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"); string result = Encoding.ASCII.GetString(source.Slice(pos, length)); pos += length; int recordSize = 2 + length; int padding = (4 - (recordSize & 3)) & 3; pos += padding; return result; } }