using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
///
/// Secure-trade GameAction builders — retail's CM_Trade senders,
/// byte-checked against ACE's readers (the server acdream runs against).
///
///
/// Wire research: docs/research/2026-08-14-trade-laneB-wire.md —
/// ACE, the retail decomp, and holtburger's independent Rust implementation
/// agree on every field ACE implements. Retail senders:
/// Event_OpenTradeNegotiations @ 0x0056D300,
/// Event_CloseTradeNegotiations @ 0x0056D1E0,
/// Event_AddToTrade @ 0x0056D0D0, Event_AcceptTrade (packing
/// Trade::Pack @ 0x005B9FF0), Event_DeclineTrade @ 0x0056D270,
/// Event_ResetTrade @ 0x0056D3D0. There is NO RemoveFromTrade
/// C→S action — retail's per-item removal is client-local; ACE's only
/// clear is the full-window ResetTrade (which clears BOTH sides).
///
public static class TradeRequests
{
public const uint GameActionEnvelope = 0xF7B1u;
public const uint OpenTradeNegotiationsOpcode = 0x01F6u;
public const uint CloseTradeNegotiationsOpcode = 0x01F7u;
public const uint AddToTradeOpcode = 0x01F8u;
public const uint AcceptTradeOpcode = 0x01FAu;
public const uint DeclineTradeOpcode = 0x01FBu;
public const uint ResetTradeOpcode = 0x0204u;
/// Open trade with — ACE walks
/// the initiator into range (CreateMoveToChain) before both sides
/// receive RegisterTrade.
public static byte[] BuildOpenTradeNegotiations(
uint gameActionSequence, uint partnerGuid)
{
byte[] body = new byte[16];
WriteHeader(body, gameActionSequence, OpenTradeNegotiationsOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), partnerGuid);
return body;
}
public static byte[] BuildCloseTradeNegotiations(uint gameActionSequence)
=> BuildEmpty(gameActionSequence, CloseTradeNegotiationsOpcode);
/// Stage an item. is ACE-ignored
/// (it always echoes slot 0); retail sends its grid slot.
public static byte[] BuildAddToTrade(
uint gameActionSequence, uint itemGuid, uint tradeSlot = 0u)
{
byte[] body = new byte[20];
WriteHeader(body, gameActionSequence, AddToTradeOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), tradeSlot);
return body;
}
///
/// Accept the current offer. The payload mirrors retail's
/// Trade::Pack fixed fields; ACE parses and then DISCARDS every
/// one of them (HandleActionAcceptTrade() takes zero arguments —
/// server state is fully self-derived), so the two trailing
/// PackableList<ContentProfile> item lists retail appends are sent
/// as zero-count lists here (register row AD-94).
///
public static byte[] BuildAcceptTrade(
uint gameActionSequence,
uint partnerGuid,
double tradeStamp,
uint tradeStatus,
uint initiatorGuid,
bool initiatorAccepts,
bool partnerAccepts)
{
byte[] body = new byte[48];
WriteHeader(body, gameActionSequence, AcceptTradeOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), partnerGuid);
BinaryPrimitives.WriteDoubleLittleEndian(body.AsSpan(16), tradeStamp);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(24), tradeStatus);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(28), initiatorGuid);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(32), initiatorAccepts ? 1u : 0u);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(36), partnerAccepts ? 1u : 0u);
// Two zero-count item lists (see remarks / AD-94).
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(40), 0u);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(44), 0u);
return body;
}
public static byte[] BuildDeclineTrade(uint gameActionSequence)
=> BuildEmpty(gameActionSequence, DeclineTradeOpcode);
/// Clear the trade window. ACE clears BOTH players' staged
/// items, not just the sender's (Player_Trade landmine — lane B §quirks).
public static byte[] BuildResetTrade(uint gameActionSequence)
=> BuildEmpty(gameActionSequence, ResetTradeOpcode);
private static byte[] BuildEmpty(uint gameActionSequence, uint opcode)
{
byte[] body = new byte[12];
WriteHeader(body, gameActionSequence, opcode);
return body;
}
private static void WriteHeader(byte[] body, uint sequence, uint opcode)
{
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), sequence);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), opcode);
}
}