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 =>
|
||||
|
|
|
|||
|
|
@ -462,31 +462,98 @@ public static class GameEvents
|
|||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0207 TradeFailure: server trade error code.</summary>
|
||||
public static uint? ParseTradeFailure(ReadOnlySpan<byte> payload)
|
||||
// ── Secure trade (docs/research/2026-08-14-trade-laneB-wire.md) ────────
|
||||
// ACE writers + retail parsers agree on every field below; retail
|
||||
// dispatch addresses cited per event.
|
||||
|
||||
/// <summary>0x01FD RegisterTrade: (initiator, partner, stamp). Retail
|
||||
/// <c>Handle_Trade__Recv_RegisterTrade @ 0x0056E050</c>. ACE landmine:
|
||||
/// BOTH sides receive initiator == partner == the non-self player's guid
|
||||
/// (never the true initiator) and stamp is always 0 — consumers must
|
||||
/// derive "who opened" themselves (lane B §quirks).</summary>
|
||||
public readonly record struct RegisterTrade(uint Initiator, uint Partner, ulong Stamp);
|
||||
|
||||
public static RegisterTrade? ParseRegisterTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 16) return null;
|
||||
return new RegisterTrade(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(payload.Slice(8)));
|
||||
}
|
||||
|
||||
/// <summary>0x01FF CloseTrade: end reason (Normal=1, EnteredCombat=2,
|
||||
/// Canceled=0x51). Retail dispatch @ 0x006ACE90.</summary>
|
||||
public static uint? ParseCloseTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0200 AddToTrade: (itemGuid, slotIndex).</summary>
|
||||
public readonly record struct AddToTrade(uint ItemGuid, uint SlotIndex);
|
||||
/// <summary>0x0200 AddToTrade: (itemGuid, side, slot). Side: 1 = the
|
||||
/// receiving client's own offer, 2 = the partner's. Slot is always 0
|
||||
/// from ACE. Retail dispatch @ 0x006ACE20 reads three dwords.</summary>
|
||||
public readonly record struct AddToTrade(uint ItemGuid, uint Side, uint SlotIndex);
|
||||
|
||||
public static AddToTrade? ParseAddToTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
if (payload.Length < 12) return null;
|
||||
return new AddToTrade(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)));
|
||||
}
|
||||
|
||||
/// <summary>0x0201 RemoveFromTrade: (itemGuid, mode). Retail
|
||||
/// <c>Handle_Trade__Recv_RemoveFromTrade @ 0x0056DC00</c>; ACE never
|
||||
/// emits it (no per-item removal server-side) — parsed defensively.</summary>
|
||||
public readonly record struct RemoveFromTrade(uint ItemGuid, uint Mode);
|
||||
|
||||
public static RemoveFromTrade? ParseRemoveFromTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
return new RemoveFromTrade(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
/// <summary>0x0202 AcceptTrade: initiator guid.</summary>
|
||||
/// <summary>0x0202 AcceptTrade: who accepted (the client compares
|
||||
/// against its own guid for self-vs-partner — retail dispatch
|
||||
/// @ 0x006ACDF0).</summary>
|
||||
public static uint? ParseAcceptTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0203 DeclineTrade: who declined.</summary>
|
||||
public static uint? ParseDeclineTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0205 ResetTrade: who reset. ACE clears BOTH sides'
|
||||
/// staged items on either player's reset (lane B §quirks).</summary>
|
||||
public static uint? ParseResetTrade(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0207 TradeFailure: (itemGuid, WeenieError reason). Retail
|
||||
/// <c>Handle_Trade__Recv_TradeFailure @ 0x0056D990</c> removes the item
|
||||
/// locally before showing the notice.</summary>
|
||||
public readonly record struct TradeFailure(uint ItemGuid, uint Reason);
|
||||
|
||||
public static TradeFailure? ParseTradeFailure(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
return new TradeFailure(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0x0264 QueryItemManaResponse: (itemGuid, manaPercent, valid).
|
||||
/// Retail anchor: <c>CM_Item::DispatchUI_QueryItemManaResponse @ 0x006A84D0</c>
|
||||
|
|
|
|||
110
src/AcDream.Core.Net/Messages/TradeRequests.cs
Normal file
110
src/AcDream.Core.Net/Messages/TradeRequests.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Secure-trade GameAction builders — retail's <c>CM_Trade</c> senders,
|
||||
/// byte-checked against ACE's readers (the server acdream runs against).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wire research: <c>docs/research/2026-08-14-trade-laneB-wire.md</c> —
|
||||
/// ACE, the retail decomp, and holtburger's independent Rust implementation
|
||||
/// agree on every field ACE implements. Retail senders:
|
||||
/// <c>Event_OpenTradeNegotiations @ 0x0056D300</c>,
|
||||
/// <c>Event_CloseTradeNegotiations @ 0x0056D1E0</c>,
|
||||
/// <c>Event_AddToTrade @ 0x0056D0D0</c>, <c>Event_AcceptTrade</c> (packing
|
||||
/// <c>Trade::Pack @ 0x005B9FF0</c>), <c>Event_DeclineTrade @ 0x0056D270</c>,
|
||||
/// <c>Event_ResetTrade @ 0x0056D3D0</c>. 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).
|
||||
/// </remarks>
|
||||
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;
|
||||
|
||||
/// <summary>Open trade with <paramref name="partnerGuid"/> — ACE walks
|
||||
/// the initiator into range (<c>CreateMoveToChain</c>) before both sides
|
||||
/// receive RegisterTrade.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Stage an item. <paramref name="tradeSlot"/> is ACE-ignored
|
||||
/// (it always echoes slot 0); retail sends its grid slot.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accept the current offer. The payload mirrors retail's
|
||||
/// <c>Trade::Pack</c> fixed fields; ACE parses and then DISCARDS every
|
||||
/// one of them (<c>HandleActionAcceptTrade()</c> 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).
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>Clear the trade window. ACE clears BOTH players' staged
|
||||
/// items, not just the sender's (Player_Trade landmine — lane B §quirks).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2561,6 +2561,65 @@ public sealed class WorldSession : IDisposable
|
|||
SendGameAction(InventoryActions.BuildDropItem(seq, itemGuid));
|
||||
}
|
||||
|
||||
// ── Secure trade (docs/research/2026-08-14-trade-laneB-wire.md) ────────
|
||||
|
||||
/// <summary>Open secure trade with another player — retail
|
||||
/// <c>CM_Trade::Event_OpenTradeNegotiations @ 0x0056D300</c>.</summary>
|
||||
public void SendOpenTradeNegotiations(uint partnerGuid)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildOpenTradeNegotiations(seq, partnerGuid));
|
||||
}
|
||||
|
||||
/// <summary>Close the trade window — <c>Event_CloseTradeNegotiations
|
||||
/// @ 0x0056D1E0</c>.</summary>
|
||||
public void SendCloseTradeNegotiations()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildCloseTradeNegotiations(seq));
|
||||
}
|
||||
|
||||
/// <summary>Stage an item into the trade — <c>Event_AddToTrade
|
||||
/// @ 0x0056D0D0</c>.</summary>
|
||||
public void SendAddToTrade(uint itemGuid, uint tradeSlot = 0u)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildAddToTrade(seq, itemGuid, tradeSlot));
|
||||
}
|
||||
|
||||
/// <summary>Accept the current offer — <c>Event_AcceptTrade</c> packing
|
||||
/// <c>Trade::Pack @ 0x005B9FF0</c>'s fixed fields (ACE discards the
|
||||
/// payload entirely; see TradeRequests.BuildAcceptTrade).</summary>
|
||||
public void SendAcceptTrade(
|
||||
uint partnerGuid,
|
||||
double tradeStamp,
|
||||
uint tradeStatus,
|
||||
uint initiatorGuid,
|
||||
bool initiatorAccepts,
|
||||
bool partnerAccepts)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildAcceptTrade(
|
||||
seq, partnerGuid, tradeStamp, tradeStatus,
|
||||
initiatorGuid, initiatorAccepts, partnerAccepts));
|
||||
}
|
||||
|
||||
/// <summary>Withdraw a previous accept — <c>Event_DeclineTrade
|
||||
/// @ 0x0056D270</c>.</summary>
|
||||
public void SendDeclineTrade()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildDeclineTrade(seq));
|
||||
}
|
||||
|
||||
/// <summary>Clear the trade window — <c>Event_ResetTrade @ 0x0056D3D0</c>.
|
||||
/// ACE clears BOTH sides' staged items (lane B §quirks).</summary>
|
||||
public void SendResetTrade()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(TradeRequests.BuildResetTrade(seq));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send retail GiveObjectRequest (0x00CD). Retail
|
||||
/// <c>CM_Inventory::Event_GiveObjectRequest @ 0x006ABB00</c> writes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue