# Secure-trade seam map (Lane C research) Read-only audit. Every claim below is anchored file:line. "NOT FOUND" means the search came up empty, not that the answer is assumed absent. ## 1. The existing option and its two current dispatch sites Enum member: `CharacterOptionId.DragItemOnPlayerOpensSecureTrade` (`src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs:134`, registered `0x04000000u`). Mirrored bit constant at `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:216` and `src/AcDream.Core.Net/Messages/SocialActions.cs:450` (`= 0x17`, a different unrelated numbering — that second one is a `CharacterOptions1Bits`/switch-ordinal, not the wire bit; don't conflate them). Read today via `RuntimeCharacterState.DragItemOnPlayerOpensSecureTrade` (`src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs:629-632,721-722`), which App threads through as a delegate in `InteractionRetainedUiComposition.cs:325-326`: ``` dragOnPlayerOpensSecureTrade: () => d.Character.Options.DragItemOnPlayerOpensSecureTrade, ``` into `ItemInteractionController`'s ctor field `_dragOnPlayerOpensSecureTrade` (`src/AcDream.App/UI/ItemInteractionController.cs:61,111,148`). **Drag-onto-player detection path.** `ItemInteractionController.PlaceIn3D` (`ItemInteractionController.cs:1034-1063`) is the drop handler for a retail inventory drag released over a world/UI target (`ItemHolder::AttemptPlaceIn3D @ 0x00588600`, per its doc comment). It builds `ItemPlacementPolicyInput` with `DragOnPlayerOpensSecureTrade: _dragOnPlayerOpensSecureTrade()` (line 1058) and calls `ItemInteractionPolicy.DecidePlacement` (line 1049). The actual branch (`src/AcDream.Core/Items/ItemInteractionPolicy.cs:372-374`): ```csharp if (input.DragOnPlayerOpensSecureTrade && target.IsPlayer) return Placement(false, new ItemPolicyAction(ItemPolicyActionKind.StartSecureTrade, input.Item.Id, target.Id, input.SplitSize)); if (target.Type == ItemType.Creature) return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.GiveToTarget, input.Item.Id, target.Id, input.SplitSize)); ``` So the option is a straight `if`: option **true** + target is a player → `StartSecureTrade` action; option **false** (or target not a player) + target is any `ItemType.Creature` (players are `ItemType.Creature` too) → `GiveToTarget` action. The vanilla give path (`GiveToTarget`) is fully wired: `ExecutePlacementActions` dispatches it through `_sendGive` → `WorldSession.SendGiveObject` (`ItemInteractionController.cs:1204-1221`, wired at `InteractionRetainedUiComposition.cs:323-324`). **`StartSecureTrade` today is a stub.** In `ExecutePlacementActions`' switch, `StartSecureTrade` has no `case` — it falls to `default:` (`ItemInteractionController.cs:1263-1268`), which invokes `_auxiliaryAction`/`PolicyActionRequested` (both effectively unhandled for this action kind — see §2) and otherwise shows the toast built by `PolicyActionMessage`: `"Secure trade is not open."` (`ItemInteractionController.cs:1296-1297`). ## 2. Use-on-selected-player today Keybind: `InputAction.UseSelected` → `SelectionInteractionController.UseCurrentSelection()` (`src/AcDream.App/Interaction/SelectionInteractionController.cs:71-73,217-233`). It enqueues `RuntimeQueuedInteractionKind.Use` via `EnqueueIdentityBound` (no target-type special-case at this layer). The queue drains through `DispatchQueuedInteraction` (`SelectionInteractionController.cs:842-865`): ```csharp case RuntimeQueuedInteractionKind.Use: _items.UseSelectedOrEnterMode(identity.ServerGuid); break; ``` `ItemInteractionController.UseSelectedOrEnterMode` (`ItemInteractionController.cs:553-563`) calls `ActivateItem(selectedObjectId)` when a selection exists. `ActivateItem` (`ItemInteractionController.cs:657-690`) builds `ItemUsePolicyInput` with `Source: Snapshot(item)` = the SELECTED object (the target player, in this scenario) and calls `ItemInteractionPolicy.DecideUse`. **The retail-cited dispatch site already classifies a selected player as OpenSecureTrade.** `ItemInteractionPolicy.DetermineUseResult` (`src/AcDream.Core/Items/ItemInteractionPolicy.cs:181-227`, cited as `ItemHolder::DetermineUseResult @ 0x00588460`): ```csharp if (ItemUseability.IsUseable(item.Useability)) return ItemPrimaryUseResult.ItemUse; if (item.IsPlayer && item.Id != playerId) return ItemPrimaryUseResult.OpenSecureTrade; ``` (lines 220-224). `DecideUse` (lines 229-312) calls this at line 239 and, because `OpenSecureTrade` (5) falls in the classified range `[PlaceInBackpack(2)..BeginGame(7)]` (line 241-242 comment: "Exact retail bound: UseObject classifies 2..7, deliberately excluding 8"), routes to `BuildUsingItemActions`, which maps `ItemPrimaryUseResult.OpenSecureTrade => ItemPolicyActionKind.OpenSecureTrade` (confirmed mapping near line 407 of the same file). So: **pressing Use with another player selected already produces an `OpenSecureTrade` policy action end-to-end through the ported retail classifier** — no new classification logic is needed. The gap is purely on the execution side: in `ExecuteUseActions`' switch, `OpenSecureTrade` has no `case` and falls to the same `default:` stub as `StartSecureTrade` (`ItemInteractionController.cs:1142-1149`), producing the identical "Secure trade is not open." toast (`PolicyActionMessage`, line 1296-1297). **Where a trade-open branch goes:** add `case ItemPolicyActionKind.OpenSecureTrade:` / `case ItemPolicyActionKind.StartSecureTrade:` to both `ExecuteUseActions` (`ItemInteractionController.cs:1072-1150`) and `ExecutePlacementActions` (`ItemInteractionController.cs:1160-1271`), each calling a new delegate (mirroring `_sendGive`) that opens the trade window / sends the wire open request with `action.TargetId`. `RetailItemConfirmationController` (`src/AcDream.App/UI/RetailItemConfirmationController.cs:41-56`) is the only current subscriber of `PolicyActionRequested`, and it only handles `ConfirmPlayerKillerSwitch`/`ConfirmNonPlayerKillerSwitch`/ `ConfirmVolatileRare` — it silently ignores `OpenSecureTrade`/ `StartSecureTrade` (`message is null` → early return, line 50-51). A new trade controller subscribing to the same event is a viable second wiring point if a dedicated ItemInteractionController delegate isn't preferred, but the delegate approach matches how `GiveToTarget` is wired (a named `_sendGive` ctor param, not the generic auxiliary-action escape hatch). ## 3. Vendor panel as the mount template `VendorUiController` (`src/AcDream.App/UI/Layout/VendorUiController.cs`) + its mount method `RetailUiRuntime.MountVendor` (`src/AcDream.App/UI/RetailUiRuntime.cs:3368-3471`). Recipe: 1. Under `_bindings.Assets.DatLock`, import the LayoutDesc via `LayoutImporter.Import(dats, VendorUiController.LayoutId, VendorUiController.RootId, ...)` (lines 3374-3382) and resolve any empty-slot sprites needed for item strips via `ItemListCellTemplate.ResolveEmptySprite` (lines 3386-3401). 2. `RetailWindowFrame.Mount(Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Vendor, Chrome = ..., Left/Top/ContentWidth/ContentHeight from root, Visible = false, Resize flags, ConstrainDragToParent/ConstrainResizeToParent, DrawChromeCenter })` (lines 3410-3434) — returns a `RetailWindowHandle`. 3. `VendorController = VendorUiController.Bind(layout, b.State, handle, b.ResolveIcon, _bindings.Inventory.Objects, _bindings.Inventory.PlayerGuid, b.ItemInteraction, b.Selection, StackSplitQuantity, _bindings.Assets.DefaultFont, _bindings.Assets.DebugFont, _bindings.Assets.ResolveSprite, emptySlotSprite, buyingEmptySlotSprite, sellingEmptySlotSprite, DialogFactory, b.DisplaySystemMessage)` (lines 3443-3462) where `b = _bindings.Vendor` (a `VendorRuntimeBindings`, `RetailUiRuntime.cs:340-354`). 4. `Host.WindowManager.AttachController(WindowNames.Vendor, VendorController)` (line 3469). Same shape used by the social panel's `MountSocialPanel` (`RetailUiRuntime.cs:2741-2965`), which additionally shows the `ActivateTabs()` call for tabbed panels (line 2929) and `_panelUi.RegisterMainPanel(...)` for panel-catalog/toolbar-button registration (lines 2956-2963) — a secure-trade window is a two-sided non-tabbed floating window like Vendor, so `MountVendor` is the closer template. **Where mount methods get called from:** `RetailUiRuntime.Initialize()` (`RetailUiRuntime.cs:455-484`) calls every `MountXxx()` in a fixed sequence, e.g. `MountSocialPanel(); MountCharacter(); MountPlugins(); MountInventory(); MountExternalContainer(); MountVendor(); MountItemCooldowns();` (lines 475-481). A `MountSecureTrade()` call would join this list, most naturally right after `MountVendor()` since both are two-participant item-exchange windows sharing icon/drag machinery. **Runtime state callback shape:** `VendorRuntimeBindings` (`RetailUiRuntime.cs:340-354`) is built in `InteractionRetainedUiComposition.cs:804-809`: ```csharp Vendor: new VendorRuntimeBindings( d.Inventory.Vendor, iconComposer.GetIcon, itemInteraction, d.Actions.Selection, text => d.Communication.AddText(text, RetailLogTextType.ClientLocal)), ``` i.e. it hands the controller the live `VendorState` object directly (not a snapshot func) plus the icon resolver, item-interaction controller, selection state, and a system-message sink. A `TradeRuntimeBindings` record would follow the identical shape: a live `RuntimeTradeState` (or its view), icon resolver, item interaction, selection, message sink. ## 4. Runtime owner shape `RuntimeInventoryState` (`src/AcDream.Runtime/Gameplay/RuntimeInventoryState.cs`) is constructed at `GameRuntime.cs:201-207`: ```csharp context.Inventory = new RuntimeInventoryState(context.EntityObjects); ``` — it takes the shared `RuntimeEntityObjectLifetime` (constructed at `GameRuntime.cs:191-199`) and exposes the SAME `ClientObjectTable` via `Objects => _entityObjects.Objects` (`RuntimeInventoryState.cs:78`); it creates no second object model. Its own children (`ExternalContainers`, `ItemMana`, `Shortcuts`, `Transactions`, `Vendor`, `VendorItems`) are constructed in its ctor (`RuntimeInventoryState.cs:53-76`) — `Vendor = new VendorState()` at line 67 is the closest existing analogue to a future `Trade` child: **vendor state lives as a child of `RuntimeInventoryState`, not as a GameRuntime-level sibling**, whereas Fellowship/Allegiance are top-level siblings (`GameRuntime.cs:236,244`). A secure-trade owner has a plausible case for either shape — it manipulates inventory items (favors the Vendor precedent, nested under `RuntimeInventoryState`) but also has its own two-party negotiation lifecycle independent of container state (favors the Fellowship/Allegiance precedent, a GameRuntime-level sibling). Either is a straight port of an existing pattern; no third shape needs inventing. **Generation reset:** `RuntimeGenerationReset` (`src/AcDream.Runtime/RuntimeGenerationReset.cs`) is constructed with every owner needing session-scoped clearing, including `_fellowship`/`_allegiance` (ctor params, lines 112-113,129-130) and drives `_inventory.ResetVendor()` at its `Vendor`-family stage (line 288) and `_fellowship.ResetSession()` / `_allegiance.ResetSession()` at their own stages (lines 317-321, enum values `Fellowship = 12`, `Allegiance = 13` at lines 41,55). A new trade owner needs either a new `ResetTrade()` call folded into the existing Vendor-family reset stage (if nested under Inventory) or its own new `RuntimeGenerationResetStage` entry + ctor param (if a GameRuntime-level sibling) — same file, same pattern either way. **`_bindings.Social`/`_bindings.Inventory` reach path (App side):** `d.Inventory.Vendor` in `InteractionRetainedUiComposition.cs:805` and `d.Runtime.Fellowship`/`d.Runtime.Allegiance` in the Social binding block (`InteractionRetainedUiComposition.cs:890-892`, `() => d.Runtime.Fellowship.Snapshot`) show the two reach patterns: a direct owned-state object (`d.Inventory.Vendor`, mutable, App reads it live) vs. a `IGameRuntimeView`-typed snapshot accessor (`d.Runtime.Fellowship.Snapshot`, immutable projection). `d.Inventory` and `d.Runtime` are both fields on `InteractionRetainedUiDependencies` (same file, referenced throughout — not independently re-verified here since both usages above are load-bearing evidence of the shape). **(a) Inbound events reaching the owner** — the FellowshipUpdate/ FriendsUpdate routing pattern, in two hops: 1. `GameEventWiring.RegisterAll` (or its per-domain overload) exposes optional `Action?` delegate holes per parsed event type, e.g. `onFellowshipUpdateFellow` (`src/AcDream.Core.Net/GameEventWiring.cs:108`, registered conditionally at lines 252-258: `registrar.Register(GameEventType.FellowshipUpdateFellow, e => { var update = GameEvents.ParseFellowshipUpdateFellow(e.Payload.Span); if (update is not null) onFellowshipUpdateFellow(update.Value); })`). 2. `LiveSessionEventRouter` (`src/AcDream.Runtime/Session/LiveSessionEventRouter.cs`) wires those holes to the Runtime owner's `Apply*` methods, conditionally on the owner being supplied (lines 256-284): ```csharp onFellowshipUpdateFellow: social.Fellowship is { } fellowshipUpdate ? fellowshipUpdate.ApplyUpdateFellow : null, ``` where `social` is a `LiveSocialSessionBindings` record (`LiveSessionEventRouter.cs:72-88`) carrying `Fellowship`/`Allegiance` owner references. 3. `LiveSessionRuntimeFactory` (`src/AcDream.App/Net/LiveSessionRuntimeFactory.cs:258-273`) constructs the router and supplies the actual owners: ```csharp var route = new LiveSessionEventRouter( ..., new LiveSocialSessionBindings( ..., Fellowship: _domain.Runtime.FellowshipOwner, Allegiance: _domain.Runtime.AllegianceOwner)); ``` `GameRuntime.FellowshipOwner`/`AllegianceOwner` are typed getters over the same `context.Fellowship`/`context.Allegiance` fields (`GameRuntime.cs:463-464`). A trade owner's inbound wiring is the same three-hop shape: parse `GameEventType.OpenTrade`/`AddToTrade`/`AcceptTrade`/etc in `GameEvents.cs` (partially started — see §5), add delegate holes + conditional registration in `GameEventWiring.cs`, add a `Trade`/`RuntimeTradeState?` field to a bindings record analogous to `LiveSocialSessionBindings`, and supply `_domain.Runtime.TradeOwner` at the `LiveSessionRuntimeFactory.cs:258-273` construction site. **(b) UI borrowing it:** the `_bindings.Social` record shape (`SocialRuntimeBindings`, `RetailUiRuntime.cs:264-286`) is a flat record of `Func` accessors + command delegates + shared `SelectionState`/ `LocalPlayerGuid` accessors, built in `InteractionRetainedUiComposition.cs:890-...` by closing over `d.Runtime.Fellowship`/`d.Runtime.Allegiance` for reads and `late.GameRuntime.FellowshipXxx(...)` (a `DeferredGameRuntimeStateCommands` instance, `src/AcDream.App/Composition/InteractionUiRuntimeSources.cs:23-140`) for generation-gated writes. Each `DeferredGameRuntimeStateCommands` method (e.g. `FellowshipCreate`, lines 126-128) calls `Invoke((commands, generation) => commands.Fellowship.Create(generation, ...))` against an `IGameRuntimeCommands` interface, whose concrete `DirectGameRuntimeCommandAdapter` implementation (`src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs:804-825` for `Create`) validates the generation token then calls the matching `WorldSession.SendXxx`. A `TradeRuntimeBindings` + trade commands on `IGameRuntimeCommands` would follow this exact three-layer shape (App binding record → `DeferredGameRuntimeStateCommands` method → `IGameRuntimeCommands.Trade.Xxx(generation, ...)` → `DirectGameRuntimeCommandAdapter` → `WorldSession.SendXxx`). ## 5. WorldSession send pattern All outbound sends in `src/AcDream.Core.Net/WorldSession.cs` follow: ```csharp uint seq = NextGameActionSequence(); SendGameAction(SomeRequests.BuildSomething(seq, ...args)); ``` Three examples: - `SendDropItem(uint itemGuid)` — `WorldSession.cs:2558-2562`. - `SendGiveObject(uint targetGuid, uint itemGuid, uint amount)` — `WorldSession.cs:2569-2574`, builder `InventoryActions.BuildGiveObjectRequest`. - `SendAppraise(uint targetGuid)` — `WorldSession.cs:2648-2652`, builder `AppraiseRequest.Build`. The builder classes live in `src/AcDream.Core.Net/Messages/` (one static class per message family, e.g. `VendorRequests.cs` for Buy/Sell, `InventoryActions.cs` for drop/give/wield). `VendorRequests.BuildBuy` (`src/AcDream.Core.Net/Messages/VendorRequests.cs:54-70+`) is the richest documented example: constants for envelope/opcode (`GameActionEnvelope = 0xF7B1u`, `BuyOpcode = 0x005Fu`) and an extensive doc comment citing the retail decompiled sender + 3 other cross-checked references for the wire layout — the expected citation depth for a new `TradeRequests.BuildOpenTrade`/`BuildAddToTrade`/etc. **NOT FOUND: no `TradeRequests`/`TradeActions` builder class exists yet** (grepped `src/` for both names — the only hits are an unrelated `IgnoreTradeRequests` character-option enum member, `src/AcDream.Core.Net/Messages/SocialActions.cs:430`). **NOT FOUND: no `WorldSession.SendXxx` for any trade opcode** (grepped `WorldSession.cs` for "Trade" — only comments about the chat "Trade" room, e.g. line 467). Every trade send must be built from scratch on this pattern. **Partial scaffolding that DOES exist:** `GameEventType` already has the ten trade opcodes (`src/AcDream.Core.Net/Messages/GameEventType.cs:62-70`: `RegisterTrade = 0x01FD`, `OpenTrade = 0x01FE`, `CloseTrade = 0x01FF`, `AddToTrade = 0x0200`, `RemoveFromTrade = 0x0201`, `AcceptTrade = 0x0202`, `DeclineTrade = 0x0203`, `ResetTrade = 0x0205`, `TradeFailure = 0x0207`, `ClearTradeAcceptance = 0x0208`), and `GameEvents.cs` has three inbound parsers already written but **unregistered** anywhere: `ParseTradeFailure` (line 466), `ParseAddToTrade` → `record struct AddToTrade(uint ItemGuid, uint SlotIndex)` (lines 472-478), `ParseAcceptTrade` (line 483-484+). `GameEventWiring.cs` has no `Register(GameEventType.OpenTrade, ...)` etc. — grepped and only found unrelated chat-room "Trade" comments. So inbound parsing is started but not wired; outbound building doesn't exist at all; `ItemPolicyObject.TradeState` (`src/AcDream.Core/Items/ItemInteractionPolicy.cs:75`) already exists as a field consumed by `DecidePlacement`/`DecideUse` (e.g. "You cannot move an item while it is being traded." at line 354, "You cannot use an item while it is being traded." at line 248) — confirming the POLICY layer already expects a `TradeState` concept even though nothing produces it live yet. ## 6. Icon rendering for item lists Vendor's shop-item rows resolve icons via a bound `Func ResolveIcon` — the same signature that `VendorRuntimeBindings.ResolveIcon` (`RetailUiRuntime.cs:342`) carries, sourced from `iconComposer.GetIcon` (`InteractionRetainedUiComposition.cs:806`). Call site in `VendorUiController` (`src/AcDream.App/UI/Layout/VendorUiController.cs:1090-1106`): ```csharp uint icon = _resolveIcon( (ItemType)(item.ItemType ?? 0u), item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); var cell = new UiItemSlot { SpriteResolve = _itemList.SpriteResolve, SlotIndex = ..., AllowDragSource = false }; cell.SetItem(item.ItemGuid, icon); ``` A second call site at `VendorUiController.cs:2199-2203` (shop item, a different list) and a third at `VendorUiController.cs:2240-2241` (`item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects` — a `ClientObject`-sourced variant, for player-owned items being sold) confirm the pattern generalizes across both "vendor stock" and "player inventory" rows — exactly the two sides a trade window needs (local player's staged items + remote player's staged items, both rendered as `UiItemSlot` rows with the same `ResolveIcon` delegate). `UiItemSlot.SetItem(guid, iconSpriteId)` is the shared cell-population call every item list in the codebase uses (vendor, external container, inventory). ## 7. Test templates - **Wire builder test (Core.Net.Tests):** `tests/AcDream.Core.Net.Tests/Messages/VendorRequestsTests.cs:1-40+` — `VendorRequestsTests.BuildBuy_SingleItem_...` asserts exact byte offsets (envelope/seq/opcode/args) via `BinaryPrimitives.ReadUInt32LittleEndian` over the returned `byte[]`. A `TradeRequestsTests.cs` would follow this shape per new opcode (OpenTrade/AddToTrade/AcceptTrade/etc). `tests/AcDream.Core.Net.Tests/Messages/FellowshipEventsTests.cs` is the matching template for the INBOUND parser side (asserting `GameEvents.ParseXxx` against constructed payload bytes). - **Runtime owner test:** `tests/AcDream.Runtime.Tests/Gameplay/RuntimeFellowshipStateTests.cs:1-30+` — constructs `GameEvents.FellowMember` fixtures and exercises full-update assembly, incremental upsert, self-vs-other removal, revision monotonicity, ownership convergence. Direct template for a `RuntimeTradeStateTests.cs`. - **Panel controller test with fixtures:** `tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs:1-40+` — a hand-built `ImportedLayout` over `RetailWindowFrame.Mount` for the behavioral suite, PLUS one real-DAT-fixture smoke test (its own doc comment cites `AppraisalUiControllerTests`'s `FixtureLoader` use) that catches drift between hardcoded element ids and the actual LayoutDesc. Direct template for a `TradeUiControllerTests.cs`. ## Summary of the actionable seam list 1. `ItemPolicyActionKind.StartSecureTrade` and `.OpenSecureTrade` are both ALREADY produced by the ported retail classifier (drag-onto-player and Use-on-selected-player respectively) — the only gap is execution. Add explicit `case` arms in `ItemInteractionController.ExecuteUseActions` (line ~1072) and `.ExecutePlacementActions` (line ~1160), each invoking a new ctor-injected delegate (mirroring `_sendGive`) rather than falling to the generic `_auxiliaryAction`/`PolicyActionRequested` stub. 2. No wire builder exists for any trade opcode — write `src/AcDream.Core.Net/Messages/TradeRequests.cs` (outbound) following `VendorRequests.cs`'s documented-citation shape, and finish `GameEvents.cs`'s partial inbound parsers (3 of ~10 opcodes started) plus register them all in `GameEventWiring.cs` (currently zero trade registrations). 3. Add `WorldSession.SendXxx` methods for each trade opcode (`WorldSession.cs`, next to `SendGiveObject`/`SendBuy`). 4. New Runtime owner `RuntimeTradeState` — decide nested-under- `RuntimeInventoryState` (Vendor precedent) vs. GameRuntime-level sibling (Fellowship/Allegiance precedent); wire construction in `GameRuntime.cs`, reset in `RuntimeGenerationReset.cs`, inbound routing through `GameEventWiring` → `LiveSessionEventRouter` → `LiveSessionRuntimeFactory.cs:258-273`, and commands through `IGameRuntimeCommands` → `DirectGameRuntimeCommandAdapter` → the new `WorldSession.SendXxx` calls. 5. New `TradeRuntimeBindings` record (mirror `VendorRuntimeBindings`, `RetailUiRuntime.cs:340-354`) built in `InteractionRetainedUiComposition.cs` alongside the `Vendor:`/`Social:` blocks, and a `TradeUiController` + `MountSecureTrade()` mounted from `RetailUiRuntime.Initialize()` next to `MountVendor()` (`RetailUiRuntime.cs:480`), reusing `ResolveIcon`/`UiItemSlot.SetItem` for both parties' staged-item rows.