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