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>
This commit is contained in:
parent
bee38b0746
commit
067cbea8a5
26 changed files with 2682 additions and 42 deletions
|
|
@ -112,7 +112,19 @@ public static class GameEventWiring
|
|||
Action<ClientCommandResponses.AllegianceUpdate>? onAllegianceUpdate = null,
|
||||
Action<uint /*weenieError*/>? onAllegianceUpdateDone = null,
|
||||
Action<uint /*weenieError*/>? onAllegianceUpdateAborted = null,
|
||||
Action<GameEvents.AllegianceLoginNotification>? onAllegianceLoginNotification = null)
|
||||
Action<GameEvents.AllegianceLoginNotification>? onAllegianceLoginNotification = null,
|
||||
// Secure trade (2026-08-14): the same Runtime-owned delegate-hole
|
||||
// shape as fellowship above. RuntimeTradeState is the consumer;
|
||||
// docs/research/2026-08-14-trade-laneB-wire.md is the wire SSOT.
|
||||
Action<GameEvents.RegisterTrade>? onTradeRegister = null,
|
||||
Action<uint /*endReason*/>? onTradeClose = null,
|
||||
Action<GameEvents.AddToTrade>? onTradeAdd = null,
|
||||
Action<GameEvents.RemoveFromTrade>? onTradeRemove = null,
|
||||
Action<uint /*whoAccepted*/>? onTradeAccept = null,
|
||||
Action<uint /*whoDeclined*/>? onTradeDecline = null,
|
||||
Action<uint /*whoReset*/>? onTradeReset = null,
|
||||
Action<GameEvents.TradeFailure>? onTradeFailure = null,
|
||||
Action? onTradeClearAcceptance = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
|
@ -316,6 +328,80 @@ public static class GameEventWiring
|
|||
});
|
||||
}
|
||||
|
||||
// ── Secure trade (0x01FD–0x0208) ──────────────────────────
|
||||
if (onTradeRegister is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.RegisterTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseRegisterTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeRegister(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeClose is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.CloseTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseCloseTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeClose(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeAdd is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.AddToTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseAddToTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeAdd(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeRemove is not null)
|
||||
{
|
||||
// ACE never emits 0x0201; registered defensively for the retail
|
||||
// handler's sake (Handle_Trade__Recv_RemoveFromTrade @ 0x0056DC00).
|
||||
registrar.Register(GameEventType.RemoveFromTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseRemoveFromTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeRemove(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeAccept is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.AcceptTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseAcceptTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeAccept(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeDecline is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.DeclineTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseDeclineTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeDecline(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeReset is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.ResetTrade, e =>
|
||||
{
|
||||
var p = GameEvents.ParseResetTrade(e.Payload.Span);
|
||||
if (p is not null) onTradeReset(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeFailure is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.TradeFailure, e =>
|
||||
{
|
||||
var p = GameEvents.ParseTradeFailure(e.Payload.Span);
|
||||
if (p is not null) onTradeFailure(p.Value);
|
||||
});
|
||||
}
|
||||
if (onTradeClearAcceptance is not null)
|
||||
{
|
||||
// 0x0208 carries no payload (GameEventClearTradeAcceptance).
|
||||
registrar.Register(GameEventType.ClearTradeAcceptance, _ =>
|
||||
onTradeClearAcceptance());
|
||||
}
|
||||
|
||||
if (onConfirmationRequest is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.CharacterConfirmationRequest, e =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue