acdream/tests/AcDream.Core.Net.Tests/Messages/TradeRequestsTests.cs
Erik 067cbea8a5 feat: secure trade with other players - wire, RuntimeTradeState, the
authored gmSecureTradeUI window, and both retail open paths

Three-lane research first (docs/research/2026-08-14-trade-lane{A,B,C}):
retail gmSecureTradeUI decode, the byte-exact ACE/decomp/holtburger
three-way wire agreement, and the acdream seam map (which found both
open paths ALREADY classified by the ported policy - OpenSecureTrade on
Use-a-player, StartSecureTrade on drag-item-onto-player with the
DragItemOnPlayerOpensSecureTrade option - dead-ending at a stub toast).

- Core.Net: TradeRequests builders (0x1F6-0x204, retail's CM_Trade
  senders byte-checked against ACE's readers; the ACE-discarded
  AcceptTrade echo carries zero-count item lists - AD-94), corrected +
  completed inbound parsers (0x1FD-0x208; the old AddToTrade parser
  missed the SIDE dword, TradeFailure missed the reason), delegate-hole
  registrars, six WorldSession sends. 10 golden-byte tests.
- Runtime: RuntimeTradeState, the third sibling J-owner (fellowship/
  allegiance shape): session-scoped, clears at generation reset (new
  stage Trade=14), staged teardown stage 11 (Identity/EntityObjects
  shift 12/13, TeardownStageCount 14 - the FA2-era per-stage-flag test
  caught the mapping exactly as designed), combined ownership ledger,
  event routing with ACE's wrong-initiator RegisterTrade landmine
  honored (partner = whichever guid is not mine). 7 conformance tests.
- App: SecureTradeUiController binds the dedicated authored LayoutDesc
  0x2100000D (root 0x1000007A - gmSecureTradeUI::PostInit's exact ids):
  partner name/status/count/grid, the authored 'Trade' accept toggle
  (accept <-> decline withdraw), 'Clear All' (ACE clears BOTH sides -
  surfaced honestly), the X close, drop-on-your-grid staging, per-mode
  accept cues (partner icon's authored Highlight state + Trade button
  Selected latch). Mounted via the vendor recipe (nine-slice chrome,
  hidden until RegisterTrade). ItemInteractionController's two policy
  arms now raise SecureTradeRequested instead of the stub toast; the
  drag path queues the dragged item until the window registers
  (ClientTradeSystem::AttemptToTradeItem @0x0056DF80's shape).

Register: AD-94 (accept-echo zero-count lists), AD-95 (numeric-only
count texts pending template verification).

Suites: App 4,990/3, Core.Net 905, Runtime 1,626 - all green. The
panel itself is user-gate acceptance (two-client connected trade), the
#372-class lesson: fixture-green alone is not acceptance for a mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 11:49:13 +02:00

143 lines
6 KiB
C#

using System;
using System.Buffers.Binary;
using AcDream.Core.Net.Messages;
using Xunit;
namespace AcDream.Core.Net.Tests.Messages;
/// <summary>
/// Golden-byte coverage for the secure-trade GameAction builders and inbound
/// parsers — the 2026-08-14 lane B wire tables (ACE + retail decomp +
/// holtburger three-way agreement).
/// </summary>
public sealed class TradeRequestsTests
{
[Fact]
public void BuildOpenTradeNegotiations_WritesEnvelopeSequenceOpcodePartner()
{
byte[] body = TradeRequests.BuildOpenTradeNegotiations(7, 0x50001234u);
Assert.Equal(16, body.Length);
Assert.Equal(TradeRequests.GameActionEnvelope,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0)));
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
Assert.Equal(0x01F6u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(0x50001234u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
}
[Theory]
[InlineData(0x01F7u)] // CloseTradeNegotiations
[InlineData(0x01FBu)] // DeclineTrade
[InlineData(0x0204u)] // ResetTrade
public void EmptyBodiedActions_WriteEnvelopeSequenceOpcodeOnly(uint opcode)
{
byte[] body = opcode switch
{
0x01F7u => TradeRequests.BuildCloseTradeNegotiations(3),
0x01FBu => TradeRequests.BuildDeclineTrade(3),
_ => TradeRequests.BuildResetTrade(3),
};
Assert.Equal(12, body.Length);
Assert.Equal(TradeRequests.GameActionEnvelope,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0)));
Assert.Equal(3u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
Assert.Equal(opcode, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
[Fact]
public void BuildAddToTrade_WritesItemAndSlot()
{
byte[] body = TradeRequests.BuildAddToTrade(5, 0x60000001u, 2u);
Assert.Equal(20, body.Length);
Assert.Equal(0x01F8u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(0x60000001u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
}
[Fact]
public void BuildAcceptTrade_WritesTradePackFixedFieldsAndTwoEmptyLists()
{
byte[] body = TradeRequests.BuildAcceptTrade(
gameActionSequence: 11,
partnerGuid: 0x50000B0Bu,
tradeStamp: 0d,
tradeStatus: 0u,
initiatorGuid: 0x5000A0A0u,
initiatorAccepts: true,
partnerAccepts: false);
Assert.Equal(48, body.Length);
Assert.Equal(0x01FAu, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(0x50000B0Bu, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
Assert.Equal(0d, BinaryPrimitives.ReadDoubleLittleEndian(body.AsSpan(16)));
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
Assert.Equal(0x5000A0A0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(28)));
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(32)));
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(36)));
// Two zero-count PackableLists (AD-94 — ACE discards the payload).
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(40)));
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(44)));
}
// ── Inbound parsers ─────────────────────────────────────────────────────
[Fact]
public void ParseRegisterTrade_ReadsInitiatorPartnerStamp()
{
byte[] payload = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x50000001u);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x50000002u);
BinaryPrimitives.WriteUInt64LittleEndian(payload.AsSpan(8), 0uL);
var parsed = GameEvents.ParseRegisterTrade(payload);
Assert.NotNull(parsed);
Assert.Equal(0x50000001u, parsed!.Value.Initiator);
Assert.Equal(0x50000002u, parsed.Value.Partner);
Assert.Equal(0uL, parsed.Value.Stamp);
Assert.Null(GameEvents.ParseRegisterTrade(payload.AsSpan(0, 12)));
}
[Fact]
public void ParseAddToTrade_ReadsGuidSideSlot()
{
byte[] payload = new byte[12];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x60000009u);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 2u);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0u);
var parsed = GameEvents.ParseAddToTrade(payload);
Assert.NotNull(parsed);
Assert.Equal(0x60000009u, parsed!.Value.ItemGuid);
Assert.Equal(2u, parsed.Value.Side);
Assert.Equal(0u, parsed.Value.SlotIndex);
Assert.Null(GameEvents.ParseAddToTrade(payload.AsSpan(0, 8)));
}
[Fact]
public void ParseTradeFailure_ReadsGuidAndReason()
{
byte[] payload = new byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x60000042u);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x426u);
var parsed = GameEvents.ParseTradeFailure(payload);
Assert.NotNull(parsed);
Assert.Equal(0x60000042u, parsed!.Value.ItemGuid);
Assert.Equal(0x426u, parsed.Value.Reason);
Assert.Null(GameEvents.ParseTradeFailure(payload.AsSpan(0, 4)));
}
[Fact]
public void SingleGuidParsers_ReadTheGuid()
{
byte[] payload = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x50000C0Cu);
Assert.Equal(0x50000C0Cu, GameEvents.ParseAcceptTrade(payload));
Assert.Equal(0x50000C0Cu, GameEvents.ParseDeclineTrade(payload));
Assert.Equal(0x50000C0Cu, GameEvents.ParseResetTrade(payload));
Assert.Equal(0x50000C0Cu, GameEvents.ParseCloseTrade(payload));
}
}