# Retail secure-trade UI (`gmSecureTradeUI`) — decomp decode Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build, PDB-named). All addresses are `acclient.exe` v11.4186 file offsets (module base ~`0x00400000`). The class is **`gmSecureTradeUI`**, not `gmTradeUI` — grep the correct name if re-deriving. Element-type-id convention observed in this file (per `DynamicCast`/ `GetUIElementType` pairs): `1`=Button, `3`(alias `0xa`? see ItemList note below)=Field-ish base, `0xc`=Text, and high `0x100000XX` values are per-class dynamic type ids assigned to more complex controls (`gmSecureTradeUI` itself is `0x10000012`, `UIElement_ItemList` is `0x10000031` (also answers to legacy id `5`), `UIElement_UIItem` (an item icon inside a list/paperdoll) is `0x10000032`). ## 1. Class shape: Create / Register / PostInit / ListenToElementMessage | Method | Address | Notes | |---|---|---| | `gmSecureTradeUI::gmSecureTradeUI` (ctor) | `0x004c97c0` | Base-inits as `UIElement_Field`, then wires in `ObjectRangeHandler` and `ItemListDragHandler` interface vtables (multiple-inheritance vtable-slot assignment — the pseudo-C shows two of these assignments mislabeled as `&gmUrgentAssistanceUI::\`vftable'` / plain vtable-thunk addresses; **this is a known Binary-Ninja mislabeling artifact, not a real base-class relationship** — trust the interface list (`ObjectRangeHandler`, `ItemListDragHandler`), not the printed symbol name). Zeroes the button/list pointer block (`memset(&m_pTradeButton, 0, 0x28)`), inits `splitItemStackSize=0`, `splitItemClassID=INVALID_DID`. | | `gmSecureTradeUI::Create` | `0x004c9890` | `operator new(0x634)` (0x634-byte instance) → ctor. | | `gmSecureTradeUI::Register` | `0x004c9cc0` | `UIElement::RegisterElementClass(0x10000012, gmSecureTradeUI::Create)`. **Element class id = `0x10000012`.** | | `gmSecureTradeUI::DynamicCast` | `0x004c96d0` | Answers `0x10000012` (self) or `3`. | | `gmSecureTradeUI::GetUIElementType` | `0x004c96f0` | Returns `0x10000012`. | | `gmSecureTradeUI::PostInit` | `0x004ca160` | See binding table below. | | `gmSecureTradeUI::ListenToElementMessage` | `0x004cae80` | See click table below. | | `gmSecureTradeUI::~gmSecureTradeUI` / scalar-deleting-dtor | `0x004c9650` / `0x004c9870` | Nothing trade-specific. | ### PostInit element bindings (`GetChildRecursive` + `DynamicCast`) All at `0x004ca160`. Order as they appear in code: | Child element id | `DynamicCast` arg | Field | Meaning | |---|---|---|---| | `0x10000086` | `1` (Button) | `m_pTradeButton` | Combined Accept/Decline action button (see click table). | | `0x10000085` | `0xc` (Text) | `m_pSelfPlayerName` | Your own name label. | | `0x10000087` | `0xc` (Text) | `m_pSelfTotalItemsLabel` | "N items" label, your side. | | `0x10000088` | `0x10000031` (ItemList) | `m_pSelfItemsList` | **Your item grid.** Also calls `UIElement_ItemList::RegisterItemListDragHandler(eax_22, &this->vtable)` — `gmSecureTradeUI` is its own drag handler. | | `0x1000007f` | *(no cast — raw `UIElement*`)* | `m_pOtherTradeStatusIndicator` | Partner "has accepted" status icon/indicator. | | `0x1000007e` | `0xc` (Text) | `m_pOtherPlayerName` | Partner's name label. | | `0x10000080` | `0xc` (Text) | `m_pOtherTotalItemsLabel` | "N items" label, partner side. | | `0x10000081` | `0x10000031` (ItemList) | `m_pOtherItemsList` | **Partner's item grid** (no drag handler registered — you can't drag out of/into it). | | `0x1000008a` | `1` (Button) | `m_pClearAllItemsButton` | "Clear all" / Reset button — same id is also read directly in `ListenToElementMessage` (see below), so it's bound to a field *and* hard-checked by id on click. | PostInit ends by tail-calling `gmSecureTradeUI::Reset(this)` unconditionally (both the found- and not-found-`0x1000008a` paths converge there). Two more ids are referenced by id only (not bound to named fields in PostInit): | id | Where used | Meaning | |---|---|---| | `0x1000008b` | `ListenToElementMessage` | Click → `this->vtable->SetVisible(0)`. No server notify — this is the panel's own close/"X" button; it just hides the window. | ### `RegisterNoticeHandler` calls in `PostInit` `PostInit` also does 13 `GlobalEventHandler::GetGlobalEventHandler()-> RegisterNoticeHandler(, &this->vtable)` calls (lines ~`0x004ca17a`– `0x004ca2e5`). **The printed `` values are decompiler noise** — most show as raw hex (`0x4dd231`…`0x4dd23a`, `0x186a8`, `0x186a9`, `0x186ab`) and one shows as a symbol (`gmKeyboardUI::ListenToElementMessage`) despite being an unrelated class's method — confirmed by checking `0x4dd230` directly: it *is* the start of `gmKeyboardUI::ListenToElementMessage` (`0x004dd230`), i.e. Binary Ninja is printing "nearest known symbol" for a raw 32-bit notice-type constant that happens to numerically fall inside `.text`. **Do not port these literal values** — they are opaque compile-time notice-type tags, not meaningful addresses. What's reliable is the *count* (13, matching the 13 `RecvNotice_*`/`Recv...` handlers below) and each handler's own address/behavior. ## 2. `RecvNotice_*` / notice handlers on `gmSecureTradeUI` | Handler | Address | Behavior | |---|---|---| | `RecvNotice_RegisterTrade(iidInitiator?, iidPartner, arg4)` | `0x004ca5c0` | Calls `SetTradePartner(this, )`, then `CPlayerSystem::RegisterObjectRangeHandler(playerSystem, &this->m_hashElementsRegisteredWith, arg3/*partner id*/, 0.0, 1, 0, 0.0, 0.0)` — **registers an object-range handler on the trade partner**, so leaving visual range auto-closes the trade (see `OnObjectRangeExit` below). This is the "trade window opened" handler. | | `RecvNotice_AddItemToTrade(itemId, side, slot)` | `0x004ca500` | `side==2` → `AddPartnerItem(itemId, slot)`; `side==1` → `AddMyItem(itemId, slot)`. | | `RecvNotice_RemoveItemFromTrade(itemId, side)` | `0x004ca630` | `side==2` → `RemovePartnerItem`; `side==1` → `RemoveAddedItem`. | | `RecvNotice_AcceptTrade(playerGuid)` | `0x004c9ce0` | `playerGuid==0`: reset-ish path — touches hash-registration bucket `[0]` (self status) then `UpdateTradeButtonState`. `playerGuid==local player`: bucket `[0]` (self) gets state `6`, then `UpdateTradeButtonState`. Otherwise (partner accepted): bucket `[4]` (== `m_pOtherTradeStatusIndicator`'s registration slot) gets state `6`. *(The "`ecx->m_hashKey->m_alphaImage(N)`" call syntax is another BN vtable-slot-mislabel — functionally this is "set the accept-status indicator element's state to N" via the notice-registration hash, most likely a plain `SetState` dispatch, not a literal alpha-image setter; treat the numeric state, not the printed method name, as ground truth.)* | | `RecvNotice_DeclineTrade(playerGuid)` | `0x004c9d70` | Mirror of Accept: `playerGuid==local player` → bucket `[0]` state `1` + `UpdateTradeButtonState`; otherwise (partner declined) → bucket `[4]` (partner indicator) state `0xd`. | | `RecvNotice_ClearTradeAcceptance()` | `0x004ca540` | Tailcalls `Reset()`. | | `RecvNotice_CloseTrade(arg2)` | `0x004ca550` | `Reset()`, then if hash bucket `[5]` is bound, sets that element's text to the empty string (`PStringBase::s_NullBuffer`) — likely clears a status/announcement text field. | | `RecvNotice_ResetTrade(arg2)` | `0x004ca670` | Tailcalls `Reset()`. | | `RecvNotice_TradeFailure(itemId, arg3)` | `0x004ca680` | `RemoveAddedItem(itemId)` — rolls back an optimistically-added self item that the server rejected. | | `RecvNotice_TradeAnItemForDummies(itemId)` | `0x004caf30` | Tailcalls `TradeAnItemForDummies(itemId)` (see below — this is the "double-click to add" convenience path, driven by a server/engine notice, not just UI). | | `RecvNotice_ServerSaysAttemptFailed(arg2)` | `0x004c98c0` | `this->m_hashElementsRegisteredWith...m_aInplaceBuckets[9] = nullptr` — clears a registration slot directly (no method call); likely cancels an in-flight optimistic-add tracking entry. Low confidence on exact semantics — flagged, not guessed. | | `RecvNotice_ServerSaysMoveItem(...)` → `ServerSaysMoveItem` | `0x004cad30` → `0x004cac20` | Full signature `(itemId, arg3, arg4, arg5, destContainerOrPlayerId, arg7, arg8, arg9)`. If `destContainerOrPlayerId == m_iidTradePartner` and `ClientTradeSystem::IsPartnerTradingItem(itemId)` and the item isn't already in `m_pOtherItemsList`: pulls the item off any pending destruction queue and calls `AddPartnerItem(itemId, ClientTradeSystem::GetItemLocationInPartnerTradeList(itemId))`. This is the "partner's item physically arrived via a container-move notice" path (items placed into the trade appear to move into a hidden container owned by the partner; this handler is what makes that show up in the partner grid). | | `RecvNotice_ItemAttributesChanged(itemId, arg3)` → `ItemAttributesChanged` | `0x004cad40` → `0x004ca9d0` | Only acts if `this->splitItemID != 0` (i.e. mid-split-for-trade) and `arg3 & 1`. If the changed item matches the pending split's class id and its new stack size equals the expected split remainder size, calls `AddItem(itemId, 0, 0, 0, 1)` and clears `splitItemID`. This is the completion of the "split stack before trading" flow started in `AcceptDragObject`. | | `OnObjectRangeExit(arg2)` | `0x004ca4c0` | If `arg2 == ClientTradeSystem::GetTradeSystem()->m_iidTradePartner`: `ClientTradeSystem::CloseTradeNegotiations()` + `Reset()`. This is the range-based auto-close wired up by `RecvNotice_RegisterTrade`'s `RegisterObjectRangeHandler` call. | | `OnVisibilityChanged(arg2)` | `0x004ca470` | On becoming hidden (need to confirm polarity from `arg2`, not fully traced) calls `Reset()`. | `Reset()` itself (`0x004ca100`): guarded by an internal bit-flag check (`(this->__inner23 >> 0x11) & 1`, likely "is this element actually constructed/active" — early-outs otherwise). Then: `SetTradePartner(0)` (clears partner name), `m_pOtherTradeStatusIndicator->SetState(0xd)` (neutral), `FlushTradeLists()`, `SetMyItemNumber()`, `SetOtherItemNumber()`, `UpdateTradeButtonState()`. `FlushTradeLists()` (`0x004c9ac0`): for every item currently in `m_pSelfItemsList`, calls `ACCWeenieObject::SetTradeState(item, 0)` (un-flags it as "in a trade") then `ItemList_Flush`. For every item in `m_pOtherItemsList`, if it's not player-owned and not already flagged container-location `0x3f00000`, queues it for destruction (`AddContentsToDestructionQueue`) — the client-side proxy objects representing the partner's offered items are throwaway and get destroyed when the trade lists are cleared. ## 3. Opening the trade panel ### Use-on-player path (confirmed) `CPlayerSystem::UsingItem(itemId, arg3, arg4)` at `0x00562f70` calls `ItemHolder::DetermineUseResult(item)` (`0x00588460`) and switches on `(result - 2)`. **`case 3` → result `5` → `ClientTradeSystem:: AttemptToOpenTradeNegotiations(GetTradeSystem(), itemId)`** at `0x00563022`. `ItemHolder::DetermineUseResult` returns `5` specifically at `0x005885f7` when: the target is *not* player-owned, has no capacity/ component-pack shortcut, isn't a "negative InqType" special object, isn't directly `ItemUses::IsUseable`, **and `esi->vtable->IsPlayer()` is true and `esi->id != `** — i.e., **"use" on any other player, with no other higher-priority use-result, resolves to "open trade."** This is the canonical "use on player → trade" trigger. `ItemHolder::DetermineUseResult` — `0x00588460`. `ClientTradeSystem::AttemptToOpenTradeNegotiations` — `0x0056dee0`: refuses (shows "You need to be in peace mode to …") if `ClientCombatSystem::GetCombatSystem()->combatMode != NONCOMBAT_COMBAT_MODE`. Otherwise sends `CM_Trade::Event_OpenTradeNegotiations(targetPlayerId)` (`0x0056df6a`) — the outbound wire request. ### Drag-item-on-player path (confirmed) `ItemHolder::AttemptPlaceIn3D(arg1, arg2, arg3)` at `0x00588600` is the generic "item dropped onto object X in the 3D view" dispatcher. At `0x005887d0`: ``` if (PlayerModule::DragItemOnPlayerOpensSecureTrade(&playerModule) != 0 && droppedOnObject->vtable->IsPlayer() != 0) { ClientTradeSystem::AttemptToTradeItem(GetTradeSystem(), targetPlayerId, droppedItemId); return 0; } ``` So the player-option gate is real and it's checked **before** the `InqType()==0x10` (give-directly) and container/lock checks that follow in the same function — dragging onto a player short-circuits straight to trade-attempt when the option is on, before any "give" logic runs. - Option getter/setter: `PlayerModule::DragItemOnPlayerOpensSecureTrade` (`0x005d31b0`) / `PlayerModule::SetDragItemOnPlayerOpensSecureTrade` (`0x005d31c0`). Player-option enum id: `DragItemOnPlayerOpensSecureTrade_PlayerOption`. StringTable label/help keys: `ID_PlayerOption_DragItemOnPlayerOpensSecureTrade` / `..._Help`, registered via `compute_str_hash` at `0x004a0f5f` / `0x004a0f85`, added to the options page via `PlayerOptionPage::AddToggleOption` at `0x004a0fa4`. `ClientTradeSystem::AttemptToTradeItem(targetPlayerId, itemId)` — `0x0056df80`: requires the item be player-owned. If already trading with someone: - same partner → `CM_Trade::SendNotice_TradeAnItemForDummies(itemId)` (adds the item directly to the already-open trade — the "convenience" add-while-already-negotiating path). - different partner → refuses with "You are already trading with som…". If not yet trading: calls `ItemHolder::UseObject(targetPlayerId, 0, 0)` (i.e. re-enters the same **use** path as above, effectively "use the target player") and stashes `attemptTradeToPlayerID` / `attemptTradeObjectID` for later. Once `ClientTradeSystem::Handle_Trade__Recv_RegisterTrade` (`0x0056e050`, the inbound "trade window opened" handler that also fires `CM_Trade::SendNotice_RegisterTrade` → `gmSecureTradeUI::RecvNotice_RegisterTrade` above) sees `m_iidTradePartner == attemptTradeToPlayerID`, it calls `AttemptToTradeItem` again to actually queue the drag-dropped item into the now-open trade. ### Internal UI-queue notice ids (bonus — confirms §1's `PostInit` id noise is noise) `CM_Trade::DispatchUI_Recv_*` (the layer between the network/engine queue and the `RecvNotice_*` UI calls) gate on small, contiguous internal tag values — these are **not** the raw wire opcodes, but they cross-check cleanly against each other and confirm the `PostInit` `RegisterNoticeHandler` hex noise (§1) is unrelated decompiler artifact, not meaningful data: | Notice | Internal tag | Dispatch fn addr | |---|---|---| | RegisterTrade | `0x1fd` | `0x006acf20` | | OpenTrade | `0x1fe` | `0x006acef0` | | CloseTrade | `0x1ff` | `0x006ace90` | | AddToTrade | `0x200` | `0x006ace20` | | RemoveFromTrade | `0x201` | `0x006acf80` | | AcceptTrade | `0x202` | `0x006ace09`/`0x006acdf0` | | DeclineTrade | `0x203` | `0x006ace60`(dispatch fn header `0x006acec0`) | | ResetTrade | `0x205` | `0x006acfb0` | | TradeFailure | `0x207` | `0x006acfe0` | | ClearTradeAcceptance | `0x208` | `0x006ace60` | ## 4. Item grids - **Your grid**: element id `0x10000088`, bound to `m_pSelfItemsList`, type `UIElement_ItemList` (element-type-id `0x10000031`). This list registers itself as an `ItemListDragHandler` target with `this` (`gmSecureTradeUI`) as handler — items are dragged **in** here by the player. Individual entries, when read back with `UIElement_ListBox::GetItem`, are `DynamicCast(0x10000032)` — a `UIElement_UIItem` icon wrapping an `ACCWeenieObject` (`weenObj` field). - **Partner's grid**: element id `0x10000081`, bound to `m_pOtherItemsList`, same `UIElement_ItemList` type, **no drag handler registered** — populated only by network notices (`AddPartnerItem`/`RemovePartnerItem`/`ServerSaysMoveItem`), not by local drag-drop. - **Counts**: `SetMyItemNumber()` (`0x004c98d0`) / `SetOtherItemNumber()` (`0x004c9970`) build a `StringInfo` with `SetStringIDandTableEnum(&info, , 0x10000001)` + `AddVariable_Int(count)` and write the resolved string into `m_pSelfTotalItemsLabel` / `m_pOtherTotalItemsLabel`. The printed `` literal is `0`, which is almost certainly the decompiler showing the static pre-initializer value of the global `ID_SecureTrade_TotalItemsLabel` (see §5) rather than the true runtime-computed hash — same class of artifact as the `PostInit` notice ids. Table-enum `0x10000001` is a StringTable category constant, not further resolved here. - **Accept/decline is presented via**: 1. `m_pTradeButton` (id `0x10000086`) — one physical button whose current `m_state` decides what a click does (see §1 click table): `m_state==6` → click triggers `AcceptTheTrade`; `m_state==1` → click triggers `DeclineTheTrade`. `UpdateTradeButtonState()` (`0x004c9700`) disables the button (state `0xd`) whenever total items across both sides is `0`, and re-enables (state `1`) once items exist. **Caution**: `UIElement_Button::SetState` (`0x00471da0`) shows this button uses a "latch" attribute pair (attrs `0xb`/`0xe`) where `SetState(6)`/`SetState(1)` toggle a latch flag and early-return *without* necessarily rewriting `m_state` to that literal value unless the latch was already in the requested position, in which case it falls through to the generic disabled-state path which does write `m_state`. The exact accept/decline toggle semantics are therefore genuinely convoluted in the retail binary; recommend the port drive the button's visual state from `ClientTradeSystem`'s own accepted/declined booleans rather than replicating this latch dance literally, and validate against a live retail trade if uncertain (this is exactly the kind of "state interacts with prior state in ways not obvious from reading" case the CLAUDE.md's cdb workflow exists for). 2. `m_pOtherTradeStatusIndicator` (id `0x1000007f`) — the partner-side accept/decline indicator icon, driven directly by `RecvNotice_AcceptTrade`/`RecvNotice_DeclineTrade` (state `6` = accepted, `0xd` = neutral/declined) and reset to `0xd` on every `AddMyItem`/`AddPartnerItem`/`RemoveAddedItem`/`RemovePartnerItem` (any list change silently un-accepts the visual, matching retail's "adding an item clears both sides' acceptance" rule). 3. Drag-affordance color feedback during hover uses the same generic accept/reject drag-state pair used elsewhere in the UI: `UIElement_UIItem::SetDragAcceptState(item, 0x10000040)` (acceptable — green) / `0x10000041` (rejected — red), set from `OnItemListDragOver` (`0x004ca980`) via `DragItemAcceptable`. `DragItemAcceptable(itemId, quiet)` (`0x004ca6a0`): if the item is player-owned and not already in the self list → acceptable. If not player-owned: acceptable only path is absent (falls to `return 0`); if `quiet==0` it also posts a rejection `StringInfo` via `ECM_UI::SendNotice_DisplayStringInfo(0x1a, …)` (the literal string text itself is unresolved — printed as a mislabeled vtable-slot symbol, another string-adjacent-to-vtable BN artifact, not a real symbol reference). `AddItem(itemId, slot, quietFlag, splitAllowedFlag, fromRecursion)` (`0x004ca780`): if the item has no contained items/containers (i.e. not itself a container), inserts it directly into `m_pSelfItemsList` and calls `ClientTradeSystem::AddItemToSelfTradeList` (the outbound wire call). If it *is* a container (has contained items) and `splitAllowedFlag` is set, it instead announces "Trading contents of %s" and recursively calls `AddItem` for every contained item — **dragging a container into the trade grid trades its contents individually, not the container itself.** `AcceptDragObject(itemId)` (`0x004caa40`): if `DragItemAcceptable` passes and the dropped item's current stack size already equals the max split size, adds it directly. Otherwise attempts `ItemHolder::AttemptToPlaceInContainer` to split off the correct amount first (stashing `splitItemID`/`splitItemClassID`/`splitItemStackSize`, announcing "Splitting the %s before trading …"); the actual add happens later when `RecvNotice_ItemAttributesChanged`/`ItemAttributesChanged` sees the split completion (§2). If the split attempt itself fails, announces "Cannot split the stack to trade …". `TradeAnItemForDummies(itemId)` (`0x004cad50`): the convenience "just trade this stack, splitting-and-all" entry point invoked from the `RecvNotice_TradeAnItemForDummies` notice (fired e.g. from `AttemptToTradeItem`'s same-partner re-add case). Refuses with "You must split the stack before …" if the item is the globally `ACCWeenieObject::selectedID` and a split is already in flight (`GenItemHolder::splitSize == maxSplitSize`); otherwise calls `AddItem` directly. `HandleDropRelease` (`0x004cae10`) / message id `0x15` in `ListenToElementMessage`: only processes a drop if the drop's ancestor chain lands inside `m_pSelfItemsList` (`UIElement::IsAncestorOfMe`) — confirms drops are only ever accepted onto your own grid, never the partner's. ## `ListenToElementMessage` click table (`0x004cae80`) | `idMessage` | `idElement` | Action | |---|---|---| | `1` (click) | `0x10000086` (trade button) | `m_state==6` → `AcceptTheTrade()`; `m_state==1` → `DeclineTheTrade()`. | | `1` (click) | `0x1000008a` (clear-all) | `ClientTradeSystem::ResetTrade(GetTradeSystem())` — outbound reset. | | `1` (click) | `0x1000008b` (close/X) | `this->vtable->SetVisible(0)` — local-only hide, no wire traffic. | | `0x15` (drop) | — | `HandleDropRelease` (self-grid-only, see §4). | `AcceptTheTrade()` (`0x004c9a10`): before sending accept, verifies the locally-displayed item counts (`GetNumUIItems` on both lists) match the server-known counts (`ClientTradeSystem::GetNumSelfObjectsInTrade`/ `GetNumPartnerObjectsInTrade`); mismatch → `ClientTradeSystem::NotifyServerThatTradeIsOutOfSync()` instead of accepting — an explicit desync guard the port should replicate. `DeclineTheTrade()` (`0x004c9a90`) is unconditional: `ClientTradeSystem::DeclineTrade()`. ## 5. StringTable keys Only one trade-panel-specific key was found via `compute_str_hash("ID_...")` scan of the whole file: | Key | Hash-init address | |---|---| | `ID_SecureTrade_TotalItemsLabel` | `0x006f2d8d` | Related but not-panel-body keys (player options / chat, not the panel itself): `ID_PlayerOption_DragItemOnPlayerOpensSecureTrade` (+`_Help`) at `0x004a0f5f`/`0x004a0f85`; `ID_PlayerOption_IgnoreTradeRequests` (+`_Help`) at `0x004a0eee`/`0x004a0f14`; `ID_ChatOption_TextFilter_Trade` (+`_Desc`) at `0x006f06cd`/`0x006f06ed`; `ID_Chat_ChatTargetMenuTrade` / `ID_Chat_TellToTrade` at `0x006f39ad`/`0x006f3a6d`. **No other `ID_SecureTrade*` or `ID_Trade*` keys exist in the pseudo-C.** Player-name and other-side labels are populated directly from `ACCWeenieObject::GetObjectNameWide` (not StringTable), and the button's own caption/tooltip text is presumably baked into the LayoutDesc/DAT authoring for element `0x10000086` rather than resolved at runtime here — the porting engineer should pull the actual authored panel (element class `0x10000012`, its children `0x1000007e`–`0x1000008b`) from the game's LayoutDesc DAT resource via the existing `LayoutImporter`/UI-Studio tooling to get real control names/captions/positions; this decomp pass only recovers behavior, not layout. ## NOT FOUND - The literal wire/network opcode for the trade `GameAction`/`GameEvent` family (as opposed to the internal `0x1fd`–`0x208` UI-queue tags in §3) was not located in `acclient.h` or the pseudo-C by direct grep; cross-check `references/ACE/` server-side trade handlers if the exact byte-level wire opcode is needed for a lane-B/network task. - The exact text of the two `StringInfo`/`ECM_UI::SendNotice_DisplayStringInfo` messages in `DragItemAcceptable` and one in `AcceptDragObject` that show as mislabeled vtable-slot symbols instead of string literals (BN string-adjacent-to-vtable artifact) — content unrecoverable from this file alone. - Full struct layout / field offsets for `gmSecureTradeUI` and `ClientTradeSystem` are not present in `acclient.h` (not reconstructed in this PDB pass); field names above are taken from the pseudo-C's own `this->fieldName` labels, which the decompiler DID resolve correctly (these are real PDB member names, unlike the notice-id/string-literal artifacts flagged above).