acdream/docs/research/2026-08-14-trade-laneA-ui.md
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

24 KiB
Raw Blame History

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(<id>, &this->vtable) calls (lines ~0x004ca17a 0x004ca2e5). The printed <id> values are decompiler noise — most show as raw hex (0x4dd2310x4dd23a, 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, <partner id via a vtable call on this-0x5f8>), 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==2AddPartnerItem(itemId, slot); side==1AddMyItem(itemId, slot).
RecvNotice_RemoveItemFromTrade(itemId, side) 0x004ca630 side==2RemovePartnerItem; side==1RemoveAddedItem.
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 0x004cad300x004cac20 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 0x004cad400x004ca9d0 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 5ClientTradeSystem:: 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 != <local player 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::DetermineUseResult0x00588460.

ClientTradeSystem::AttemptToOpenTradeNegotiations0x0056dee0: 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_RegisterTradegmSecureTradeUI::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, <ID>, 0x10000001) + AddVariable_Int(count) and write the resolved string into m_pSelfTotalItemsLabel / m_pOtherTotalItemsLabel. The printed <ID> 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==6AcceptTheTrade(); m_state==1DeclineTheTrade().
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 0x1000007e0x1000008b) 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 0x1fd0x208 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).