fix(vendor): gate-findings pass — the X button HIDES like retail, clicks return, the dropdown scrolls, pyreal suffix, staged-tab slots
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
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 user's connected gate found five issues; each fixed at the root: G4 (the discovery): retail's vendor X button calls only SetVisible(0) (pc:204147-204182) — the SESSION stays open and re-using the vendor lands on the same-session refresh; the range watcher remains the sole real close. Our port invented a full teardown on X, which is exactly why reopening died. The Runtime fixture proves the wire dispatch was never the problem; ACE has no already-open short-circuit. G3 (regression from the drag-suppression fix): denying IsDragSource also dropped press capture, so clicks fell through to window-drag. UiItemSlot.HandlesClick now claims presses for any occupied cell independent of drag eligibility — clickable and draggable are separate concerns. G5: the authored popup 0x21000043 is ONE scrollable column with a real scrollbar subtree (live-dat scan: ListBox 0x10000350 + scrollbar 0x10000351), not a 3x6 grid. UiMenu gains an authored-driven Scrollable mode (wheel, thumb drag, track paging, up/down buttons); chat's menu is untouched and its ten tests prove it. G1: retail's cost format is "%s %hsp (you have %hsp)" — the p after each %hs is a LITERAL pyreal suffix the port swallowed as part of the specifier. Restored. G2: the Buying/Selling pages' authored lists (same cell template as Items) get the empty-slot fill, presentation-only until staging. Clean-room complete solution: 11,390 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
58c8de264e
commit
5224e43890
8 changed files with 1061 additions and 51 deletions
|
|
@ -70,6 +70,82 @@ public sealed class VendorUiControllerTests
|
|||
Assert.NotNull(controller);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_FromRealDatFixture_BuyingAndSellingLists_ConfiguredForEmptySlotFill()
|
||||
{
|
||||
// G2 (vendor gate finding): the Buying/Selling pages' item strips
|
||||
// never got the empty-slot fill the Items list has, so they showed
|
||||
// the bare authored blue background instead. This proves the real
|
||||
// LayoutDesc 0x21000012 fixture's 0x100000C5 (Buying list)/0x100000CE
|
||||
// (Selling list) resolve to real UiItemList widgets and come out of
|
||||
// Bind configured identically to the Items strip (F7b) — same
|
||||
// single-row/horizontal-scroll/cell-size shape, fill enabled, a
|
||||
// non-drag-source empty-slot factory wired, and the sibling
|
||||
// scrollbar bound to the SAME list's scroll model. The lists stay
|
||||
// UNPOPULATED (no AddItem call anywhere in this path) — staging is
|
||||
// still deferred.
|
||||
ImportedLayout layout = FixtureLoader.LoadVendor();
|
||||
var screen = new UiRoot { Width = 1280f, Height = 800f };
|
||||
RetailWindowHandle window = RetailWindowFrame.Mount(
|
||||
screen,
|
||||
layout.Root,
|
||||
static _ => (0u, 0, 0),
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = "vendor-fixture-smoke-2",
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
var objects = new ClientObjectTable();
|
||||
using var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: static () => 0u,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null);
|
||||
VendorUiController? controller = VendorUiController.Bind(
|
||||
layout,
|
||||
new VendorState(),
|
||||
window,
|
||||
static (_, _, _, _, _) => 0u,
|
||||
objects,
|
||||
static () => 0u,
|
||||
itemInteraction,
|
||||
new SelectionState(),
|
||||
new StackSplitQuantityState(),
|
||||
datFont: null,
|
||||
debugFont: null,
|
||||
static _ => (0u, 0, 0));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
var buyingList = Assert.IsType<UiItemList>(layout.FindElement(VendorUiController.BuyingListId));
|
||||
var sellingList = Assert.IsType<UiItemList>(layout.FindElement(VendorUiController.SellingListId));
|
||||
var buyingScrollbar = Assert.IsType<UiScrollbar>(
|
||||
layout.FindElement(VendorUiController.BuyingScrollbarId));
|
||||
var sellingScrollbar = Assert.IsType<UiScrollbar>(
|
||||
layout.FindElement(VendorUiController.SellingScrollbarId));
|
||||
|
||||
foreach (UiItemList list in new[] { buyingList, sellingList })
|
||||
{
|
||||
Assert.True(list.SingleRow);
|
||||
Assert.True(list.HorizontalScroll);
|
||||
Assert.Equal(32f, list.CellWidth);
|
||||
Assert.Equal(32f, list.CellHeight);
|
||||
Assert.True(list.FillVisibleEmptySlots);
|
||||
Assert.NotNull(list.EmptySlotFactory);
|
||||
Assert.Equal(0, list.GetNumUIItems()); // never populated
|
||||
}
|
||||
|
||||
Assert.Same(buyingList.Scroll, buyingScrollbar.Model);
|
||||
Assert.True(buyingScrollbar.Horizontal);
|
||||
Assert.Same(sellingList.Scroll, sellingScrollbar.Model);
|
||||
Assert.True(sellingScrollbar.Horizontal);
|
||||
}
|
||||
|
||||
private sealed class Harness
|
||||
{
|
||||
// F2/F3: a deterministic non-zero player coin total so the cost-text
|
||||
|
|
@ -310,7 +386,7 @@ public sealed class VendorUiControllerTests
|
|||
// to the singular name UNCHANGED (not an invented "Arrows" + "s").
|
||||
Assert.Equal("100 Arrows", GetText(h.ItemNameText));
|
||||
Assert.Equal(
|
||||
$"cost {2000:N0} (you have {Harness.DefaultPlayerCoinValue:N0})",
|
||||
$"cost {2000:N0}p (you have {Harness.DefaultPlayerCoinValue:N0}p)",
|
||||
GetText(h.ItemCostText));
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +414,7 @@ public sealed class VendorUiControllerTests
|
|||
|
||||
Assert.Equal("Bread", GetText(h.ItemNameText));
|
||||
Assert.Equal(
|
||||
$"costs {20:N0} (you have {Harness.DefaultPlayerCoinValue:N0})",
|
||||
$"costs {20:N0}p (you have {Harness.DefaultPlayerCoinValue:N0}p)",
|
||||
GetText(h.ItemCostText));
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +462,7 @@ public sealed class VendorUiControllerTests
|
|||
// selection.
|
||||
Assert.Equal("Arrows", GetText(h.ItemNameText));
|
||||
Assert.Equal(
|
||||
$"costs {20:N0} (you have {Harness.DefaultPlayerCoinValue:N0})",
|
||||
$"costs {20:N0}p (you have {Harness.DefaultPlayerCoinValue:N0}p)",
|
||||
GetText(h.ItemCostText));
|
||||
|
||||
// Player drags the slider to 40 AFTER selecting -- no re-click, no
|
||||
|
|
@ -396,7 +472,7 @@ public sealed class VendorUiControllerTests
|
|||
// SellPrice = ceil(2.0*10*40 - 0.1) = 800.
|
||||
Assert.Equal("40 Arrows", GetText(h.ItemNameText));
|
||||
Assert.Equal(
|
||||
$"cost {800:N0} (you have {Harness.DefaultPlayerCoinValue:N0})",
|
||||
$"cost {800:N0}p (you have {Harness.DefaultPlayerCoinValue:N0}p)",
|
||||
GetText(h.ItemCostText));
|
||||
|
||||
h.BuyButton.OnClick!.Invoke();
|
||||
|
|
@ -576,6 +652,45 @@ public sealed class VendorUiControllerTests
|
|||
Assert.NotEqual(0u, cell.ItemId); // occupied -- would otherwise be a drag source by default
|
||||
Assert.False(cell.IsDragSource);
|
||||
Assert.Null(cell.GetDragPayload());
|
||||
// G3 (vendor gate finding): a row must still CAPTURE its own press
|
||||
// even though it never mints a drag payload -- see
|
||||
// UiItemSlot.HandlesClick and the real-event-path test below.
|
||||
Assert.True(cell.HandlesClick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShopRow_ClickEvent_SelectsItem_DespiteNotBeingADragSource()
|
||||
{
|
||||
// G3 (vendor gate finding): the F3 drag-suppression fix
|
||||
// (AllowDragSource=false) left occupied vendor rows with
|
||||
// IsDragSource==false -- before the G3 fix that meant UiRoot's
|
||||
// mousedown dispatch found no reason to claim the press at all, so
|
||||
// it fell through to the window-move fallback (hover showed the
|
||||
// move-window cursor; a press dragged the whole panel instead of
|
||||
// selecting a row). This drives the REAL UiItemSlot.OnEvent state
|
||||
// machine (MouseDown then Click), the same sequence UiRoot's
|
||||
// dispatch produces, rather than invoking the wired Clicked
|
||||
// delegate directly -- proving the row still completes a press then
|
||||
// click and drives selection.
|
||||
const uint SecondArmorGuid = 0x60000110u;
|
||||
var h = new Harness();
|
||||
h.State.Apply(VendorGuid, Profile(), new[]
|
||||
{
|
||||
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
|
||||
new VendorShopItem(SecondArmorGuid, -1, 4u, "Buckler", (uint)ItemType.Armor, 900u, 40),
|
||||
});
|
||||
Assert.Equal(ArmorItemGuid, h.Selection.SelectedObjectId); // auto-selected first
|
||||
|
||||
UiItemSlot secondCell = h.ItemList.GetItem(1)!;
|
||||
Assert.Equal(SecondArmorGuid, secondCell.ItemId);
|
||||
Assert.False(secondCell.IsDragSource); // never a drag source (F3)
|
||||
Assert.True(secondCell.HandlesClick); // but still claims its own press (G3)
|
||||
|
||||
secondCell.OnEvent(new UiEvent(0u, secondCell, UiEventType.MouseDown));
|
||||
secondCell.OnEvent(new UiEvent(0u, secondCell, UiEventType.Click));
|
||||
|
||||
Assert.Equal(SecondArmorGuid, h.Selection.SelectedObjectId);
|
||||
Assert.True(secondCell.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -720,8 +835,22 @@ public sealed class VendorUiControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseButton_RoutesThroughVendorStateClose_NotADirectFieldWrite()
|
||||
public void CloseButton_HidesTheWindowOnly_LeavesTheSessionOpenForARefreshInPlaceReopen()
|
||||
{
|
||||
// G4 (vendor gate finding): retail's close/pushpin button —
|
||||
// gmVendorUI::HandleButtonClicks's 0x100000d6 case (pc:204147-204182)
|
||||
// — with nothing staged in the Buying/Selling lists (this port never
|
||||
// stages anything; Slice 6 territory) calls ONLY SetVisible(0),
|
||||
// never gmVendorUI::CloseVendor (pc:202080, the range-watcher-
|
||||
// unregister/session-teardown function VendorState.Close ports).
|
||||
// The OLD port called VendorState.Close() directly from this button
|
||||
// — an over-eager full teardown retail does not perform on an
|
||||
// ordinary close. RuntimeVendorRangeQuery.EnforceRange (evaluated
|
||||
// every frame regardless of window visibility) remains the sole
|
||||
// path to a full close once the player actually leaves UseRadius —
|
||||
// see Closed_HidesWindowAndClearsListAndText for that path,
|
||||
// unaffected by this change since it calls VendorState.Close()
|
||||
// directly rather than through this button.
|
||||
var h = new Harness();
|
||||
h.State.Apply(VendorGuid, Profile(), new[]
|
||||
{
|
||||
|
|
@ -730,12 +859,25 @@ public sealed class VendorUiControllerTests
|
|||
|
||||
h.CloseButton.OnClick!.Invoke();
|
||||
|
||||
// The panel never mutates VendorState directly — the ONLY path from
|
||||
// the close button to a cleared session is VendorState.Close()
|
||||
// itself, so VendorId reads back 0 through the owner's own public
|
||||
// surface, not a private field poke.
|
||||
Assert.Equal(0u, h.State.VendorId);
|
||||
Assert.False(h.Window.IsVisible);
|
||||
// Unlike the OLD port, the session itself is NOT torn down — the
|
||||
// owner still reports the same open vendor in the background,
|
||||
// matching retail's hidden-but-still-registered range watcher.
|
||||
Assert.Equal(VendorGuid, h.State.VendorId);
|
||||
|
||||
// Re-approaching the SAME vendor (e.g. pressing Use again while
|
||||
// still in range) now reaches retail's sameVendor==1 refresh-in-
|
||||
// place path (VendorStateTransitionKind.Refreshed) instead of a
|
||||
// from-scratch Opened, and reopens the window.
|
||||
var kinds = new List<VendorStateTransitionKind>();
|
||||
h.State.Changed += t => kinds.Add(t.Kind);
|
||||
h.State.Apply(VendorGuid, Profile(), new[]
|
||||
{
|
||||
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
|
||||
});
|
||||
|
||||
Assert.True(h.Window.IsVisible);
|
||||
Assert.Equal([VendorStateTransitionKind.Refreshed], kinds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue