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

@ -365,12 +365,17 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
// Slice 6.3: ItemInteractionController.TryBuy owns the
// reservation dance itself (see its doc comment); this is a
// plain wire send, not a second requestUse-shaped delegate.
// F4 (Slice 6 review): report whether the send actually
// happened — a null CurrentSession or a not-in-world session
// must return false so TryBuy releases the reservation instead
// of marking it dispatched for a request nothing ever sent.
sendBuy: (vendorGuid, itemGuid, amount, alternateCurrencyId) =>
session.CurrentSession?.SendBuy(
vendorGuid,
itemGuid,
amount,
alternateCurrencyId));
{
if (session.CurrentSession is not { } activeSession || !session.IsInWorld)
return false;
activeSession.SendBuy(vendorGuid, itemGuid, amount, alternateCurrencyId);
return true;
});
}
public MagicRuntime CreateMagicRuntime(
@ -671,8 +676,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
// guid" — Slice 6.1 guarantees a materialized shop
// item's ContainerId IS the vendor's guid, so this reads
// straight off the same ClientObjectTable/VendorState
// pair VendorUiController.VendorSplitSize's display-only
// copy also reads, through the SAME VendorSplitPolicy
// pair VendorUiController.ResolveBuyQuantity (F2, Slice 6
// review) also reads, through the SAME VendorSplitPolicy
// mask helper (no second mask copy).
guid =>
d.Inventory.Vendor.VendorId != 0u