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