fix: trade gate round 3 - the mount-time-captured dead command bus (ONE

root cause for every dead interaction) + retail's Total Items caption

The round-2 probes nailed it: the request seam fired for BOTH open
paths (use AND drag - "drag-release pick" -> "drag-on-player" ->
"request"), but no open-cmd, no wire-open, and no LiveCommandBus
drop-warning ever printed. MountSecureTrade captured
_bindings.Options.CommandBus() ONCE at mount time - the pre-session
surface whose Publish routes into a null route silently. CommandBus is
a Func for exactly this reason; the social mounts resolve it inside
each lambda. Every trade command - open (use + drag), accept (the
"unpressable" Trade button - the click FIRED, the publish died),
Clear All, close, and drop-on-grid staging - died on that one captured
bus. All six lambdas now resolve the Func per call.

Also: ID_SecureTrade_TotalItemsLabel probe-verified token-free
(fragments ["Total Items: ", ""], one ITEMS variable 0x004E8A23) and
composed via ResolveTemplate - the count texts read retail's exact
"Total Items: N". AD-95 RETIRED same-day.

The pre-feature stub-toast test row (drag-on-player option-on expecting
"Secure trade is not open.") now pins the SecureTradeRequested seam
instead. App suite 4,991/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 12:34:31 +02:00
parent f2144ab2a4
commit ffc73f80bf
6 changed files with 97 additions and 16 deletions

View file

@ -63,7 +63,11 @@ public sealed class SecureTradeUiController : IRetainedPanelController
// ItemListCellTemplate from the lists' own 0x1000000E cell-template
// attribute (0x1000033A) — same recipe as the vendor strips.
uint SelfEmptySlotSprite = 0u,
uint PartnerEmptySlotSprite = 0u);
uint PartnerEmptySlotSprite = 0u,
// ID_SecureTrade_TotalItemsLabel composed per count — probe-verified
// token-free (fragments ["Total Items: ", ""], one ITEMS variable);
// null falls back to the bare number.
Func<int, string>? FormatTotalItems = null);
private readonly Bindings _bindings;
private readonly UiText? _partnerName;
@ -255,10 +259,11 @@ public sealed class SecureTradeUiController : IRetainedPanelController
private void SetCount(UiText? text, int count)
{
if (text is null) return;
// Numeric-only, the AD-85 numeric-fields disposition: the authored
// ID_SecureTrade_TotalItemsLabel template's variable shape is
// unverified, so the DATA shows without invented surrounding words.
string line = count.ToString();
// Retail's exact ID_SecureTrade_TotalItemsLabel — probe-verified
// token-free (gate round 3), composed through the same
// ResolveTemplate the confirmation dialogs use.
string line = _bindings.FormatTotalItems?.Invoke(count)
?? count.ToString();
text.LinesProvider = () => [new UiText.Line(line, Vector4.One)];
}

View file

@ -3524,7 +3524,11 @@ public sealed class RetailUiRuntime : IDisposable
return;
}
var bus = _bindings.Options.CommandBus();
// Gate round 3 root cause: CommandBus is a Func for a REASON — the
// live route attaches at session start, AFTER this mount. Capturing
// the surface once here published every trade command into the
// pre-session null route, silently (the exact reason the social
// mount resolves the Func inside each lambda). Resolve per call.
Layout.SecureTradeUiController? controller =
Layout.SecureTradeUiController.Bind(
layout,
@ -3532,24 +3536,43 @@ public sealed class RetailUiRuntime : IDisposable
Trade: tradeView,
Objects: _bindings.Inventory.Objects,
ResolveIcon: _bindings.Inventory.ResolveIcon,
OpenTrade: partner => bus.Publish(
OpenTrade: partner => _bindings.Options.CommandBus().Publish(
new OpenTradeNegotiationsRuntimeCmd(partner)),
CloseTrade: () => bus.Publish(
CloseTrade: () => _bindings.Options.CommandBus().Publish(
new CloseTradeNegotiationsRuntimeCmd()),
AddToTrade: item => bus.Publish(
AddToTrade: item => _bindings.Options.CommandBus().Publish(
new AddToTradeRuntimeCmd(item)),
AcceptTrade: (selfAccepted, partnerAccepted, partner) =>
bus.Publish(new AcceptTradeRuntimeCmd(
_bindings.Options.CommandBus().Publish(new AcceptTradeRuntimeCmd(
partner, selfAccepted, partnerAccepted)),
DeclineTrade: () => bus.Publish(new DeclineTradeRuntimeCmd()),
ResetTrade: () => bus.Publish(new ResetTradeRuntimeCmd()),
DeclineTrade: () => _bindings.Options.CommandBus().Publish(
new DeclineTradeRuntimeCmd()),
ResetTrade: () => _bindings.Options.CommandBus().Publish(
new ResetTradeRuntimeCmd()),
SetWindowVisible: visible =>
{
if (visible) Host.ShowWindow(WindowNames.SecureTrade);
else Host.HideWindow(WindowNames.SecureTrade);
},
SelfEmptySlotSprite: selfEmptySlotSprite,
PartnerEmptySlotSprite: partnerEmptySlotSprite));
PartnerEmptySlotSprite: partnerEmptySlotSprite,
// ID_SecureTrade_TotalItemsLabel (probe-verified
// token-free; the ITEMS variable) — retires AD-95.
FormatTotalItems: count =>
{
lock (_bindings.Assets.DatLock)
{
var strings = new DatStringResolver(_bindings.Assets.Dats);
return strings.ResolveTemplate(
0x23000001u,
"ID_SecureTrade_TotalItemsLabel",
new Dictionary<uint, string>
{
[DatStringResolver.ComputeHash("ITEMS")] =
count.ToString(),
}) ?? count.ToString();
}
}));
if (controller is null)
{
Console.WriteLine("[M4] secure trade: required authored grids are missing.");