using System; using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Messages; /// /// Outbound social + query GameActions. These share a common pattern: /// single-target / single-parameter GameActions inside the 0xF7B1 /// envelope. /// /// /// Wire format: /// /// u32 0xF7B1 envelope /// u32 gameActionSequence /// u32 subOpcode /// <payload> per-action /// /// /// /// /// References: r08 §3 rows for each opcode. /// /// public static class SocialActions { public const uint GameActionEnvelope = 0xF7B1u; // Queries public const uint QueryHealthOpcode = 0x01BFu; // u32 targetGuid public const uint PingRequestOpcode = 0x01E9u; // u32 clientId // Fellowship public const uint FellowshipCreateOpcode = 0x00A2u; // string16L name, bool openness, bool shareXP public const uint FellowshipQuitOpcode = 0x00A3u; // bool disband public const uint FellowshipDismissOpcode = 0x00A4u; // u32 guid public const uint FellowshipRecruitOpcode = 0x00A5u; // u32 guid public const uint FellowshipUpdateOpcode = 0x00A6u; // bool open // Character options public const uint SetCharacterOptionsOpcode = 0x01A1u; // u32 options bitmap // Chat channels public const uint AddChannelOpcode = 0x0145u; // string16L channelName public const uint RemoveChannelOpcode = 0x0146u; // string16L channelName /// Query a target's health — server replies with UpdateHealth (0x01C0). public static byte[] BuildQueryHealth(uint seq, uint targetGuid) { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), QueryHealthOpcode); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid); return body; } /// Keepalive ping — server echoes with PingResponse (0x01EA). public static byte[] BuildPingRequest(uint seq, uint clientId) { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), PingRequestOpcode); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), clientId); return body; } /// Create a fellowship with a chosen name + options. public static byte[] BuildFellowshipCreate( uint seq, string fellowshipName, bool openness, bool shareXp) { byte[] name = PackString16L(fellowshipName); // 2 bools consume 2 bytes + alignment pad to 4. int boolBlock = 2; int pad = (4 - ((name.Length + boolBlock) & 3)) & 3; byte[] body = new byte[12 + name.Length + boolBlock + pad]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipCreateOpcode); Array.Copy(name, 0, body, 12, name.Length); body[12 + name.Length] = openness ? (byte)1 : (byte)0; body[12 + name.Length + 1] = shareXp ? (byte)1 : (byte)0; return body; } /// Quit your current fellowship (optionally disband if leader). public static byte[] BuildFellowshipQuit(uint seq, bool disband) { byte[] body = new byte[16]; // envelope + 1 byte bool aligned to 4 BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipQuitOpcode); body[12] = disband ? (byte)1 : (byte)0; return body; } /// Dismiss a specific vassal from your fellowship. public static byte[] BuildFellowshipDismiss(uint seq, uint targetGuid) => SingleGuid(seq, FellowshipDismissOpcode, targetGuid); /// Recruit a target into your fellowship. public static byte[] BuildFellowshipRecruit(uint seq, uint targetGuid) => SingleGuid(seq, FellowshipRecruitOpcode, targetGuid); /// Toggle fellowship open / closed recruiting. public static byte[] BuildFellowshipUpdate(uint seq, bool open) { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateOpcode); body[12] = open ? (byte)1 : (byte)0; return body; } /// Push the client's character-options bitmap to the server. public static byte[] BuildSetCharacterOptions(uint seq, uint optionsBitmap) { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), SetCharacterOptionsOpcode); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), optionsBitmap); return body; } /// Subscribe to a named chat channel. public static byte[] BuildAddChannel(uint seq, string channelName) => SingleString(seq, AddChannelOpcode, channelName); /// Unsubscribe from a named chat channel. public static byte[] BuildRemoveChannel(uint seq, string channelName) => SingleString(seq, RemoveChannelOpcode, channelName); // ── Helpers ────────────────────────────────────────────────────────────── private static byte[] SingleGuid(uint seq, uint sub, uint guid) { byte[] body = new byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), sub); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), guid); return body; } private static byte[] SingleString(uint seq, uint sub, string s) { byte[] str = PackString16L(s); byte[] body = new byte[12 + str.Length]; BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), sub); Array.Copy(str, 0, body, 12, str.Length); return body; } private static byte[] PackString16L(string s) { ArgumentNullException.ThrowIfNull(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); return result; } }