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>
23 KiB
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):
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):
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):
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:
- Under
_bindings.Assets.DatLock, import the LayoutDesc viaLayoutImporter.Import(dats, VendorUiController.LayoutId, VendorUiController.RootId, ...)(lines 3374-3382) and resolve any empty-slot sprites needed for item strips viaItemListCellTemplate.ResolveEmptySprite(lines 3386-3401). 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 aRetailWindowHandle.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) whereb = _bindings.Vendor(aVendorRuntimeBindings,RetailUiRuntime.cs:340-354).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:
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:
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:
GameEventWiring.RegisterAll(or its per-domain overload) exposes optionalAction<T>?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); })).LiveSessionEventRouter(src/AcDream.Runtime/Session/LiveSessionEventRouter.cs) wires those holes to the Runtime owner'sApply*methods, conditionally on the owner being supplied (lines 256-284):
whereonFellowshipUpdateFellow: social.Fellowship is { } fellowshipUpdate ? fellowshipUpdate.ApplyUpdateFellow : null,socialis aLiveSocialSessionBindingsrecord (LiveSessionEventRouter.cs:72-88) carryingFellowship/Allegianceowner references.LiveSessionRuntimeFactory(src/AcDream.App/Net/LiveSessionRuntimeFactory.cs:258-273) constructs the router and supplies the actual owners:var route = new LiveSessionEventRouter( ..., new LiveSocialSessionBindings( ..., Fellowship: _domain.Runtime.FellowshipOwner, Allegiance: _domain.Runtime.AllegianceOwner));GameRuntime.FellowshipOwner/AllegianceOwnerare typed getters over the samecontext.Fellowship/context.Allegiancefields (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<TSnapshot> 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:
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, builderInventoryActions.BuildGiveObjectRequest.SendAppraise(uint targetGuid)—WorldSession.cs:2648-2652, builderAppraiseRequest.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<ItemType, uint, uint, uint, uint, uint> 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):
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) viaBinaryPrimitives.ReadUInt32LittleEndianover the returnedbyte[]. ATradeRequestsTests.cswould follow this shape per new opcode (OpenTrade/AddToTrade/AcceptTrade/etc).tests/AcDream.Core.Net.Tests/Messages/FellowshipEventsTests.csis the matching template for the INBOUND parser side (assertingGameEvents.ParseXxxagainst constructed payload bytes). - Runtime owner test:
tests/AcDream.Runtime.Tests/Gameplay/RuntimeFellowshipStateTests.cs:1-30+— constructsGameEvents.FellowMemberfixtures and exercises full-update assembly, incremental upsert, self-vs-other removal, revision monotonicity, ownership convergence. Direct template for aRuntimeTradeStateTests.cs. - Panel controller test with fixtures:
tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs:1-40+— a hand-builtImportedLayoutoverRetailWindowFrame.Mountfor the behavioral suite, PLUS one real-DAT-fixture smoke test (its own doc comment citesAppraisalUiControllerTests'sFixtureLoaderuse) that catches drift between hardcoded element ids and the actual LayoutDesc. Direct template for aTradeUiControllerTests.cs.
Summary of the actionable seam list
ItemPolicyActionKind.StartSecureTradeand.OpenSecureTradeare both ALREADY produced by the ported retail classifier (drag-onto-player and Use-on-selected-player respectively) — the only gap is execution. Add explicitcasearms inItemInteractionController.ExecuteUseActions(line ~1072) and.ExecutePlacementActions(line ~1160), each invoking a new ctor-injected delegate (mirroring_sendGive) rather than falling to the generic_auxiliaryAction/PolicyActionRequestedstub.- No wire builder exists for any trade opcode — write
src/AcDream.Core.Net/Messages/TradeRequests.cs(outbound) followingVendorRequests.cs's documented-citation shape, and finishGameEvents.cs's partial inbound parsers (3 of ~10 opcodes started) plus register them all inGameEventWiring.cs(currently zero trade registrations). - Add
WorldSession.SendXxxmethods for each trade opcode (WorldSession.cs, next toSendGiveObject/SendBuy). - New Runtime owner
RuntimeTradeState— decide nested-under-RuntimeInventoryState(Vendor precedent) vs. GameRuntime-level sibling (Fellowship/Allegiance precedent); wire construction inGameRuntime.cs, reset inRuntimeGenerationReset.cs, inbound routing throughGameEventWiring→LiveSessionEventRouter→LiveSessionRuntimeFactory.cs:258-273, and commands throughIGameRuntimeCommands→DirectGameRuntimeCommandAdapter→ the newWorldSession.SendXxxcalls. - New
TradeRuntimeBindingsrecord (mirrorVendorRuntimeBindings,RetailUiRuntime.cs:340-354) built inInteractionRetainedUiComposition.csalongside theVendor:/Social:blocks, and aTradeUiController+MountSecureTrade()mounted fromRetailUiRuntime.Initialize()next toMountVendor()(RetailUiRuntime.cs:480), reusingResolveIcon/UiItemSlot.SetItemfor both parties' staged-item rows.