fix(vendor): grand-gate findings — wire-truth container counts, the live split bar, arrival-gated use, prepend-order race
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
Four live findings, each with the paper-verification failure named: G1 the container-capacity guard counted containers by a local type/capacity heuristic that over-classifies ordinary items; retail buckets from the wire's ContainerProperties at insert. Now reads ClientObjectTable's existing ContainerTypeHint (AP-168 narrowed to the shop-stock half; a pre-check must never false-block). G2 the amount bar never showed live because ACE never sets StackSize on browse listings — DescStackSize is null for every real vendor item and the C4 paper test hand-set the field, bypassing the materializer. The materializer now falls back to the packed supply count (AP-169, ACE adaptation); the new test drives the REAL materializer. G3 an out-of-range Use now dispatches ON ARRIVAL (pickup's shape): ACE's HandleActionUseItem only opens the vendor when the Use finds the player in range — a click-time send is greeted and dropped (AP-170, ACE adaptation; retail's server walks the player, ACE does not). G4 bought items appended because ACE's placement echo (UIQueue) can beat the CreateObject (SmartboxQueue) — cross-queue, no ordering guarantee — and the early echo was silently dropped. ClientObjectTable now stashes unresolved placements and replays them at Ingest: buys land at the retail list head. No register row — this RESTORES parity. Clean-room complete solution: 11,521 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c68ad1e646
commit
68568a3a59
12 changed files with 1140 additions and 90 deletions
|
|
@ -1623,16 +1623,28 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
/// reference <c>0x007b57b4</c>; <c>pc:204056</c>/<c>204068</c> both
|
||||
/// reference <c>0x007b5750</c> via the shared <c>label_4c5509</c>).
|
||||
/// <para>
|
||||
/// <b>Container-vs-item slot classification (register AP-168).</b>
|
||||
/// Retail's own split tests a bitfield bit this codebase does not
|
||||
/// currently thread onto <see cref="VendorShopItem"/>
|
||||
/// (<c>gmVendorUI::InqListSlotCount</c>, <c>pc:200038-200065</c>) — this
|
||||
/// port approximates "is this shop item a container" with
|
||||
/// <see cref="ItemType.Container"/> instead, correct for the ordinary
|
||||
/// case (a real backpack/pouch DOES carry that type bit) but not
|
||||
/// byte-identical for the theoretical case of a non-<c>Container</c>-typed
|
||||
/// item that still authors nonzero pack/side capacities. See the
|
||||
/// register.
|
||||
/// <b>Container-vs-item slot classification (register AP-168, narrowed
|
||||
/// G1 gate-finding fix 2026-08-08).</b> Retail's own split tests a
|
||||
/// bitfield bit this codebase does not currently thread onto
|
||||
/// <see cref="VendorShopItem"/> (<c>gmVendorUI::InqListSlotCount</c>,
|
||||
/// <c>pc:200038-200065</c>) — this port approximates "is this shop item
|
||||
/// a container" with <see cref="ItemType.Container"/> instead, correct
|
||||
/// for the ordinary case (a real backpack/pouch DOES carry that type
|
||||
/// bit) but not byte-identical for the theoretical case of a
|
||||
/// non-<c>Container</c>-typed item that still authors nonzero pack/side
|
||||
/// capacities. This residual applies ONLY to the shop-stock side
|
||||
/// (<see cref="ComputeBuySlotsNeeded"/>) — <see cref="VendorShopItem"/>
|
||||
/// genuinely has no wire-carried classification field to read instead.
|
||||
/// The player's-OWN-pack side (<see cref="CountPlayerContents"/>) no
|
||||
/// longer shares this approximation: it now reads
|
||||
/// <see cref="ClientObject.ContainerTypeHint"/> (retail's actual wire
|
||||
/// <c>ContainerProperties</c>, already threaded onto every owned object)
|
||||
/// first, matching retail's real <c>_itemsList</c>/<c>_containersList</c>
|
||||
/// bucketing exactly for anything that ever received a hint. Live
|
||||
/// testing showed the OLD dual-heuristic (also checking nonzero
|
||||
/// <c>ItemsCapacity</c>/<c>ContainersCapacity</c>) could over-classify a
|
||||
/// non-container object as an occupied container slot and false-block a
|
||||
/// purchase with real free slots — see the register.
|
||||
/// </para>
|
||||
/// On a successful DISPATCH the whole staged list is flushed
|
||||
/// UNCONDITIONALLY and immediately, matching retail's literal order:
|
||||
|
|
@ -1733,7 +1745,35 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
return (itemSlots, containerSlots);
|
||||
}
|
||||
|
||||
/// <summary>F1: the player's CURRENT occupied item/container slot counts.</summary>
|
||||
/// <summary>
|
||||
/// F1 (G1 gate-finding fix, 2026-08-08): the player's CURRENT occupied
|
||||
/// item/container slot counts. Retail's own <c>GetNumContainedItems</c>/
|
||||
/// <c>GetNumContainedContainers</c> (<c>0x0058beb0</c>/<c>0x0058bec0</c>)
|
||||
/// don't reclassify anything at count time — they just report the length
|
||||
/// of two ALREADY-BUCKETED <c>IDList</c>s (<c>_itemsList</c>/
|
||||
/// <c>_containersList</c>). The bucketing happens once, at INSERT time
|
||||
/// (<c>ACCWeenieObject::ServerSaysContainID @ 0x0058be40</c>), from the
|
||||
/// wire's own <c>ContainerProperties</c> field (<c>Item_ServerSaysContainId</c>
|
||||
/// 0x0022's <c>ContainerType</c>; also carried by <c>ContentProfile</c> /
|
||||
/// <c>PlayerDescription</c>'s per-entry container-kind byte) — a
|
||||
/// None/Container/Foci discriminator the SERVER computes, not something
|
||||
/// the client reverse-engineers from the item's own type bits.
|
||||
/// <see cref="ClientObject.ContainerTypeHint"/> is exactly that wire
|
||||
/// field, already threaded through every membership path
|
||||
/// (<c>InitializeInventoryManifest</c>, <c>InventoryPutObjInContainer</c>,
|
||||
/// <c>ViewContents</c>) and already used for this identical
|
||||
/// container-vs-item question elsewhere
|
||||
/// (<c>ClientObjectTable.IsContainerListMember</c>). This port previously
|
||||
/// used ONLY the local <see cref="ItemType.Container"/>/capacity-field
|
||||
/// heuristic here (AP-168) and never consulted the hint — live testing
|
||||
/// showed that guessing wrong in the OVER-classify direction (a
|
||||
/// non-container object whose capacity fields happen to read nonzero)
|
||||
/// false-blocks a purchase with real free slots (#G1). The hint is now
|
||||
/// authoritative when present; the heuristic is a narrower fallback
|
||||
/// (Container-typed only, matching <see cref="ComputeBuySlotsNeeded"/>'s
|
||||
/// single signal) for the rare object that reached the table without
|
||||
/// ever threading a hint.
|
||||
/// </summary>
|
||||
private (int Items, int Containers) CountPlayerContents()
|
||||
{
|
||||
int items = 0, containers = 0;
|
||||
|
|
@ -1741,9 +1781,7 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
{
|
||||
ClientObject? obj = _objects.Get(guid);
|
||||
bool isContainer = obj is not null
|
||||
&& (obj.ItemsCapacity > 0
|
||||
|| obj.ContainersCapacity > 0
|
||||
|| (obj.Type & ItemType.Container) != 0);
|
||||
&& (obj.ContainerTypeHint != 0u || (obj.Type & ItemType.Container) != 0);
|
||||
if (isContainer) containers++; else items++;
|
||||
}
|
||||
return (items, containers);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue