using System; using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Messages; /// /// Outbound chat GameActions — Talk (local /say), Tell (whisper), /// ChatChannel (allegiance / general). /// /// /// All three ride inside the 0xF7B1 GameAction envelope with /// the same leading fields: /// /// u32 0xF7B1 /// u32 gameActionSequence /// u32 subOpcode /// <payload> /// /// /// /// /// String16L wire shape mirrors the other outbound AC messages: /// /// u16 length // byte count (not char count) /// byte[] ascii // ASCII bytes, no terminator /// pad to 4-byte boundary /// /// /// /// /// Source of truth: r08 §3 rows 0x0015 / 0x005D / 0x0147. /// /// public static class ChatRequests { public const uint GameActionEnvelope = 0xF7B1u; public const uint TalkOpcode = 0x0015u; public const uint TellOpcode = 0x005Du; public const uint ChatChannelOpcode = 0x0147u; /// Send a local /say message (heard by anyone within ~20m). public static byte[] BuildTalk(uint gameActionSequence, string message) { ArgumentNullException.ThrowIfNull(message); byte[] msg = PackString16L(message); byte[] body = new byte[12 + msg.Length]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TalkOpcode); Array.Copy(msg, 0, body, 12, msg.Length); return body; } /// Send a /tell (whisper) by target character name. /// /// Wire field order is message FIRST, then target — that's /// what ACE's GameActionTell.Handle reads (see /// references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionTell.cs /// lines 17-18: /// /// var message = clientMessage.Payload.ReadString16L(); /// var target = clientMessage.Payload.ReadString16L(); /// /// Filed after a 2026-04-25 live trace where every /tell +Je hello /// failed with WeenieError 0x052B (CharacterNotAvailable). With the /// previous (target-first) wire shape, ACE was reading our message /// payload "hello" as the recipient name, looking it up, finding no /// character "hello" online, and returning the not-available error. /// Swapping the field order makes /tell <name> <text> /// actually deliver. /// public static byte[] BuildTell(uint gameActionSequence, string targetName, string message) { ArgumentNullException.ThrowIfNull(targetName); ArgumentNullException.ThrowIfNull(message); byte[] msg = PackString16L(message); byte[] name = PackString16L(targetName); byte[] body = new byte[12 + msg.Length + name.Length]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TellOpcode); Array.Copy(msg, 0, body, 12, msg.Length); Array.Copy(name, 0, body, 12 + msg.Length, name.Length); return body; } /// Send to a chat channel (allegiance, general, trade, etc). public static byte[] BuildChatChannel(uint gameActionSequence, uint channelId, string message) { ArgumentNullException.ThrowIfNull(message); byte[] msg = PackString16L(message); byte[] body = new byte[16 + msg.Length]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), ChatChannelOpcode); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), channelId); Array.Copy(msg, 0, body, 16, msg.Length); return body; } private static byte[] PackString16L(string s) { // Windows-1252 matches retail (and holtburger's encoding_rs::WINDOWS_1252). byte[] data = Encoding.GetEncoding(1252).GetBytes(s); if (data.Length > ushort.MaxValue) throw new ArgumentException("String too long for 16-bit length prefix.", nameof(s)); int recordSize = 2 + data.Length; int padding = (4 - (recordSize & 3)) & 3; byte[] result = new byte[recordSize + padding]; BinaryPrimitives.WriteUInt16LittleEndian(result, (ushort)data.Length); Array.Copy(data, 0, result, 2, data.Length); // trailing bytes are already zero from new[] return result; } }