fix(vendor): Slice 6 review corrections — ownership-checked retire, live slider display, drag-proof shop rows, hardened buy reservation
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

All nine findings from the buy-arc review, at root:

F1 the materializer's retire pass re-checks ownership (guid->vendorId
map; remove only while the live object's ContainerId still equals the
recording vendor) — buying a player-sold UNIQUE no longer deletes the
item you just purchased; the discriminating reparent-then-refresh test
pins it. F2 the cost/name display subscribes to the live split state
and shares ONE quantity computation with Buy (retail re-renders per
slider tick: RecvNotice_StackSliderChanged 0x004C4500) — the sentence
and the charge can no longer disagree. F3 shop rows never mint drag
payloads (UiItemSlot.AllowDragSource gates both IsDragSource AND
GetDragPayload — the second gate was caught by this pass's own test).
F4 sendBuy reports whether anything was sent; a null-session buy
cancels the reservation instead of leaking BusyCount forever.
F5 the retire loop snapshots, isolates per-guid observer failures, and
clears its tracking in finally and Dispose — teardown convergence can
no longer wedge. F6 auto-select is retail's unconditional
first-filtered-item shape (pc:201180-201184; the survival-check was
our invention and the comment claiming otherwise is corrected).
F7 non-stack buys clamp to quantity 1 locally (BuySingleItem
pc:201669). F8 the Add button is hard-disabled until staging exists.
F9 AP-161/162/163 rewritten to the post-fix reality.

Clean-room complete solution: 11,378 passed / 4 skipped / 0 failed.
The #350 render-ledger overflow observed this session is under
separate investigation and is NOT addressed here.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-07 23:12:50 +02:00
parent 97cf873870
commit 3c9fc57adb
11 changed files with 670 additions and 116 deletions

View file

@ -140,7 +140,7 @@ public class UiItemSlot : UiElement
/// <inheritdoc/>
public override object? GetDragPayload()
=> ItemId != 0 && !_primaryPressConsumed
=> AllowDragSource && ItemId != 0 && !_primaryPressConsumed
? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut)
: null;
@ -158,7 +158,8 @@ public class UiItemSlot : UiElement
internal override void SetDragSourceActive(bool active, object? payload)
{
// ItemList_BeginDrag ghosts physical lists, but explicitly excludes shortcut lists
// (along with vendor/salvage lists, which acdream does not model as ItemDragSource).
// (along with vendor/salvage lists — see AllowDragSource above, F3: those never
// reach here at all, since IsDragSource is false for them).
// Keep the source's full cell icon in place and reveal the authored grey mesh over it.
SetWaitingState(active && SourceKind != ItemDragSource.ShortcutBar);
}
@ -171,13 +172,35 @@ public class UiItemSlot : UiElement
internal void SetWaitingState(bool waiting)
=> _waiting = waiting && ItemId != 0;
/// <summary>
/// F3 (Slice 6 review): opt-out for lists whose rows must NEVER initiate
/// a drag, regardless of occupancy — retail's <c>ItemList_BeginDrag</c>
/// explicitly excludes vendor/salvage lists from drag-drop (see the
/// <see cref="SetDragSourceActive"/> comment above). Before shop items
/// had real <see cref="ClientObjectTable"/> identity (Slice 6.1) a
/// dragged shop guid failed every destination's existence guard as a
/// harmless no-op; once it resolved, the SAME drag would pass a pack
/// drop's <c>PutItemInContainer</c>, persist a dangling shortcut-bar
/// entry, or fire <c>PlaceIn3D</c> — reparenting a vendor's stock
/// without ever going through Buy. Gating the SOURCE here (rather than
/// asking every destination handler to reject a vendor-tagged payload)
/// means the drag never starts at all, so no future drop handler can
/// regress this by forgetting a check. Defaults true — every existing
/// physical list (inventory, paperdoll, container, shortcut bar) is
/// unaffected; <see cref="VendorUiController"/> is the only caller that
/// sets it false.
/// </summary>
public bool AllowDragSource { get; set; } = true;
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
/// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar
/// movable by its empty cells / chrome. Drives <see cref="UiRoot"/>'s mousedown
/// window-vs-item disambiguation (retail moves the window via a dragbar, never cells;
/// our whole-window-drag approximation reconciles by gating on occupancy).</summary>
public override bool IsDragSource => ItemId != 0;
/// our whole-window-drag approximation reconciles by gating on occupancy).
/// <see cref="AllowDragSource"/> is an additional, independent gate (F3) — false for
/// vendor rows regardless of ItemId.</summary>
public override bool IsDragSource => ItemId != 0 && AllowDragSource;
/// <summary>Walk up to the containing <see cref="UiItemList"/> (the drop handler owner).</summary>
protected UiItemList? FindList()