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