feat(ui): Slice 5.4 — the authored vendor browse panel (LayoutDesc 0x21000012)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

The vendor window is retail's own: LayoutDesc 0x21000012, root
0x100000B7, found by enumerating all 101 layouts for the one
containing both known tab controls and clinched by the root's Type
0x10000017 — the literal UIElement::RegisterElementClass id for
gmVendorUI (pc:202075). Discovery evidence and the D0 read live in
the research doc's new §B.4.

D0 corrected two assumptions: retail's category "tabs" are a UiMenu
DROPDOWN fed by a hardcoded 18-row ordered category table (ported
bit-for-bit against our ItemType enum; list always scoped to exactly
one category, first-present wins, selection preserved across refresh
per retail's clamp), and the layout authors THREE tabs — Items
(browse, this slice), Buying and Selling (staged-transaction review,
Slice 6) — decision 4's "browse/Buy tab" names the Items tab retail's
mode-2 OpenTab opens. The non-default tabs render and switch pages
but stay inert, fenced in comments.

VendorUiController mounts Items: category dropdown, icon-cell item
row with the retained scrollbar, per-unit retail pricing via
VendorPricing.SellPrice (the vendor-stock path VendorProfile::
VendorSellPrice feeds), name/cost on selection. The panel is a pure
projection of VendorState — opens on populate, closes on clear; the
close button's VendorState.Close() is its only permitted mutation.
Nothing on the wire.

AP-110 narrowed (vendor leaves the absent-panels list); AP-161 files
the precise Slice-6 remainder (Buying/Selling unwired, Buy/Add
buttons, InqAcceptability). Twelve controller tests on a real-dat
fixture. Clean-room complete solution: 11,323 passed / 4 skipped /
0 failed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-07 17:00:21 +02:00
parent 609a2dfda0
commit c721830e71
10 changed files with 17024 additions and 2 deletions

View file

@ -168,6 +168,10 @@ public sealed record AppraisalRuntimeBindings(
Action<uint, string> SendSetInscription,
Action<string> DisplaySystemMessage);
public sealed record VendorRuntimeBindings(
VendorState State,
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon);
public sealed record RetailUiRuntimeBindings(
UiHost Host,
RetailUiAssets Assets,
@ -184,6 +188,7 @@ public sealed record RetailUiRuntimeBindings(
CharacterRuntimeBindings Character,
InventoryRuntimeBindings Inventory,
ExternalContainerRuntimeBindings ExternalContainer,
VendorRuntimeBindings Vendor,
RetailUiCursorBindings Cursor,
ConfirmationRuntimeBindings Confirmations,
AppraisalRuntimeBindings Appraisal,
@ -255,6 +260,7 @@ public sealed class RetailUiRuntime : IDisposable
MountPlugins();
MountInventory();
MountExternalContainer();
MountVendor();
MountItemCooldowns();
Host.WindowManager.WindowVisibilityChanged += OnWindowVisibilityChanged;
BindToolbarPanelButtons();
@ -272,6 +278,7 @@ public sealed class RetailUiRuntime : IDisposable
WindowNames.Combat,
WindowNames.JumpPowerbar,
WindowNames.ExternalContainer,
WindowNames.Vendor,
]);
}
@ -322,6 +329,7 @@ public sealed class RetailUiRuntime : IDisposable
public InventoryController? InventoryPanelController { get; private set; }
public RetailDialogFactory? DialogFactory { get; private set; }
public ExternalContainerController? ExternalContainerController { get; private set; }
public VendorUiController? VendorController { get; private set; }
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
{
@ -1897,6 +1905,75 @@ public sealed class RetailUiRuntime : IDisposable
"[D.2b] retail external-container strip mounted from LayoutDesc 0x21000008.");
}
/// <summary>
/// Slice 5.4: the retail vendor "Items" browse tab (LayoutDesc
/// 0x21000012, root 0x100000B7 — see
/// docs/research/2026-08-08-slice5-vendor-browse-research.md §B.3/§B.4
/// for the discovery + D0 category-filter read). Two-step mount
/// (Mount then Bind then AttachController) matches
/// <see cref="MountExternalContainer"/>: the controller needs the
/// <see cref="RetailWindowHandle"/> to Show/Hide itself as
/// <see cref="VendorState"/> opens/closes, so the handle must exist
/// before the controller is constructed.
/// </summary>
private void MountVendor()
{
ImportedLayout? layout;
lock (_bindings.Assets.DatLock)
{
layout = LayoutImporter.Import(
_bindings.Assets.Dats,
VendorUiController.LayoutId,
VendorUiController.RootId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
if (layout is null)
{
Console.WriteLine("[M4] vendor: LayoutDesc 0x21000012 root 0x100000B7 not found.");
return;
}
UiElement root = layout.Root;
RetailWindowHandle handle = RetailWindowFrame.Mount(
Host.Root,
root,
_bindings.Assets.ResolveSprite,
new RetailWindowFrame.Options
{
WindowName = WindowNames.Vendor,
// Root 0x100000B7 authors only the tiled center surface
// (backdrop element 0x1000008D provides its own fill); the
// common floating-window bevel surrounds it — same shape as
// ExternalContainerController's 0x21000008/0x10000063.
Chrome = RetailWindowChrome.NineSlice,
Left = root.Left,
Top = root.Top,
ContentWidth = root.Width,
ContentHeight = root.Height,
MinWidth = root.Width,
MinHeight = root.Height,
Visible = false,
ResizeX = false,
ResizeY = false,
ConstrainDragToParent = true,
ConstrainResizeToParent = true,
DrawChromeCenter = false,
});
VendorRuntimeBindings b = _bindings.Vendor;
VendorController = VendorUiController.Bind(layout, b.State, handle, b.ResolveIcon);
if (VendorController is null)
{
Console.WriteLine("[M4] vendor: required authored controls are missing.");
return;
}
Host.WindowManager.AttachController(WindowNames.Vendor, VendorController);
Console.WriteLine("[M4] retail vendor browse panel mounted from LayoutDesc 0x21000012.");
}
private void MountItemCooldowns()
{
ItemCooldownAssets? assets;