acdream/tests/AcDream.Runtime.Tests/Gameplay/RuntimeTradeStateTests.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

142 lines
5.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AcDream.Core.Net.Messages;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Tests.Gameplay;
/// <summary>
/// Secure-trade owner conformance (2026-08-14): the 0x01FD0x0208 event
/// family against the lane B wire truths — including ACE's
/// wrong-initiator RegisterTrade, its both-sides ResetTrade, and retail's
/// remove-before-notice TradeFailure handling.
/// </summary>
public sealed class RuntimeTradeStateTests
{
private const uint Self = 0x50000001u;
private const uint Partner = 0x50000002u;
private const uint ItemA = 0x60000001u;
private const uint ItemB = 0x60000002u;
[Fact]
public void RegisterOpensAndDerivesPartnerDespiteAceGuidLandmine()
{
using var trade = new RuntimeTradeState();
// ACE sends initiator == partner == the non-self guid on BOTH sides.
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
RuntimeTradeSnapshot snapshot = trade.View.Snapshot;
Assert.True(snapshot.IsOpen);
Assert.Equal(Partner, snapshot.PartnerGuid);
Assert.False(snapshot.SelfAccepted);
Assert.False(snapshot.PartnerAccepted);
}
[Fact]
public void AddStagesPerSideAndDropsBothAcceptances()
{
using var trade = new RuntimeTradeState();
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.ApplyAccept(Self, Self);
trade.ApplyAccept(Partner, Self);
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
trade.ApplyAdd(new GameEvents.AddToTrade(ItemB, (uint)RuntimeTradeSide.Partner, 0u));
// Duplicate echo is a no-op stage-wise.
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
RuntimeTradeSnapshot snapshot = trade.View.Snapshot;
Assert.Equal(1, snapshot.SelfItemCount);
Assert.Equal(1, snapshot.PartnerItemCount);
Assert.Equal([ItemA], trade.View.GetItems(RuntimeTradeSide.Self));
Assert.Equal([ItemB], trade.View.GetItems(RuntimeTradeSide.Partner));
// Staging invalidates prior acceptance (the ClearTradeAcceptance
// mirror).
Assert.False(snapshot.SelfAccepted);
Assert.False(snapshot.PartnerAccepted);
}
[Fact]
public void AcceptDeclineTrackSelfVersusPartnerByGuid()
{
using var trade = new RuntimeTradeState();
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.ApplyAccept(Partner, Self);
Assert.True(trade.View.Snapshot.PartnerAccepted);
Assert.False(trade.View.Snapshot.SelfAccepted);
trade.ApplyAccept(Self, Self);
Assert.True(trade.View.Snapshot.SelfAccepted);
trade.ApplyDecline(Partner, Self);
Assert.False(trade.View.Snapshot.PartnerAccepted);
Assert.True(trade.View.Snapshot.SelfAccepted);
}
[Fact]
public void ResetClearsBothSidesButKeepsTheWindowOpen()
{
using var trade = new RuntimeTradeState();
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
trade.ApplyAdd(new GameEvents.AddToTrade(ItemB, (uint)RuntimeTradeSide.Partner, 0u));
trade.ApplyAccept(Self, Self);
trade.ApplyReset();
RuntimeTradeSnapshot snapshot = trade.View.Snapshot;
Assert.True(snapshot.IsOpen);
Assert.Equal(0, snapshot.SelfItemCount);
Assert.Equal(0, snapshot.PartnerItemCount);
Assert.False(snapshot.SelfAccepted);
}
[Fact]
public void FailureRemovesTheItemAndRecordsTheReason()
{
using var trade = new RuntimeTradeState();
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
trade.ApplyFailure(new GameEvents.TradeFailure(ItemA, 0x426u));
RuntimeTradeSnapshot snapshot = trade.View.Snapshot;
Assert.Equal(0, snapshot.SelfItemCount);
Assert.Equal(ItemA, snapshot.LastFailureItemGuid);
Assert.Equal(0x426u, snapshot.LastFailureReason);
}
[Fact]
public void CloseAndGenerationClearConvergeTheLedger()
{
var trade = new RuntimeTradeState();
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
trade.ApplyClose();
Assert.False(trade.View.Snapshot.IsOpen);
Assert.Equal(0, trade.View.Snapshot.SelfItemCount);
trade.ApplyRegister(new GameEvents.RegisterTrade(Partner, Partner, 0uL), Self);
trade.Clear();
Assert.False(trade.View.Snapshot.IsOpen);
trade.Dispose();
Assert.True(trade.CaptureOwnership().IsConverged);
}
[Fact]
public void EventsBeforeRegisterAreIgnored()
{
using var trade = new RuntimeTradeState();
trade.ApplyAdd(new GameEvents.AddToTrade(ItemA, (uint)RuntimeTradeSide.Self, 0u));
trade.ApplyAccept(Partner, Self);
trade.ApplyReset();
RuntimeTradeSnapshot snapshot = trade.View.Snapshot;
Assert.False(snapshot.IsOpen);
Assert.Equal(0, snapshot.SelfItemCount);
Assert.False(snapshot.PartnerAccepted);
}
}