fix(vendor): re-gate residuals — MaxStackSize is the stack operand, wire-authored use radius, purse summaries
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

R1 the split bar's operand is the item's authored MaxStackSize —
three retail sites read pwd._maxStackSize directly (InqListSlotCount
pc:200052, buy-button cases pc:203996/204086) where ACE never fills
the desc stack and standard stock is unlimited. Threaded StackSizeMax
end to end with one shared resolver; the two literal _maxStackSize
sites are now byte-exact; AP-165 retired, AP-169 corrected.
R2 walk-to-vendor never opened because GetUseRadius used an UNCITED
3m Creature heuristic as the local stop distance while ACE's poll
demands the authored radius (default 0.6 m) — the walk stopped and
the Use fired far outside acceptance. Now reads the wire-authored
spawn UseRadius with ACE's exact fallback; heuristic constants
deleted. A first sabotage attempt was non-discriminating
(coincidental 0.6) and was corrected — the discriminating version is
what landed.
R3 the Buying/Selling purse summaries ("Buying %d %s worth %hsp" /
"You have %hsp") recovered from the binary data segment where BN
mis-attributes the Buy-side literal; wired to staging and money
changes on the four authored text elements; AP-166 narrowed to the
pending-sell highlight.

Clean-room complete solution: 11,528 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 15:23:10 +02:00
parent 68568a3a59
commit d003449bb4
11 changed files with 680 additions and 103 deletions

View file

@ -674,7 +674,8 @@ public class SelectedObjectControllerTests
// ══════════════════════════════════════════════════════════════════════
/// <summary>
/// G2 (grand-gate finding): C4 passed by manually hand-setting
/// G2 (grand-gate finding), CORRECTED for R1 (2026-08-08, register
/// AP-169). C4 passed by manually hand-setting
/// <c>ClientObject.StackSize</c> directly, bypassing BOTH the real
/// <see cref="VendorShopItemMaterializer"/> that populates it from a
/// live <c>ApproachVendor</c> snapshot AND the real
@ -685,12 +686,25 @@ public class SelectedObjectControllerTests
/// production's <c>RuntimeInventoryState</c> constructor) ingests the
/// shop item into the SAME <see cref="ClientObjectTable"/>, and only
/// THEN is the item selected — closing the gap the C4 harness left open.
/// <para>
/// R1 correction: the G2 fix's own live re-test showed its
/// packed-supply-count fallback (this test's ORIGINAL fixture: packed
/// <c>StackSize=100</c>, a bounded finite quantity) does not survive a
/// STANDARD retail listing, which has UNLIMITED stock (packed
/// <c>StackSize=-1</c>) — the fallback produced nothing usable and the
/// live retail screenshot that triggered the re-test showed the real
/// operand is the item's authored <see cref="VendorShopItem.MaxStackSize"/>
/// (1000 for a Prismatic Taper), not any supply count. This fixture now
/// matches that exact ACE-realistic shape: unlimited packed supply, no
/// <c>DescStackSize</c> (ACE never sets it — see the class doc above),
/// <c>MaxStackSize=1000</c> from the wire.
/// </para>
/// </summary>
[Fact]
public void G2_VendorStackSelection_ThroughRealMaterializer_ShowsSplitSlider()
{
const uint vendorGuid = 0x70000011u;
const uint arrowsGuid = 0x60009011u;
const uint taperGuid = 0x60009011u;
ImportedLayout layout = FixtureLoader.LoadToolbar();
var objects = new ClientObjectTable();
@ -730,41 +744,46 @@ public class SelectedObjectControllerTests
&& vendorCandidate.ContainerId == vendor.VendorId
&& VendorSplitPolicy.IsSplitExempt(vendorCandidate.Type));
// G2 root cause: a REAL ACE vendor listing, byte-for-byte. ACE's
// Vendor.LoadInventoryItem (Vendor.cs:144-172) builds the browse-list
// WorldObject via WorldObjectFactory.CreateNewWorldObject and sets
// ONLY wo.VendorShopCreateListStackSize = stackSize ?? -1 (the "how
// many available" packed dword — our VendorShopItem.StackSize) —
// it NEVER calls wo.SetStackSize(...), so the per-item
// G2 root cause, R1-corrected: a REAL ACE vendor listing,
// byte-for-byte. ACE's Vendor.LoadInventoryItem (Vendor.cs:144-172)
// builds the browse-list WorldObject via
// WorldObjectFactory.CreateNewWorldObject and sets ONLY
// wo.VendorShopCreateListStackSize = stackSize ?? -1 (the "how many
// available" packed dword — our VendorShopItem.StackSize) — it
// NEVER calls wo.SetStackSize(...), so the per-item
// PublicWeenieDesc's own conditional StackSize field (our
// DescStackSize — retail's pwd._stackSize, what
// GameEventApproachVendor.cs:60's SerializeGameDataOnly walks) comes
// back null on the real wire. StackSize=100 (packed "100 for sale"),
// DescStackSize=null (ACE never sets it) is what a real vendor
// listing looks like, NOT the DescStackSize=100 shape used above.
// back null on the real wire. A STANDARD listing (unlimited stock,
// packed StackSize=-1) is the common case the live re-test actually
// hit — matching the retail screenshot's Prismatic Taper, not a
// bounded-supply item like the original fixture's "100 arrows".
// MaxStackSize=1000 IS reliably populated by ACE (an ordinary
// weenie property), which is the field the toolbar ceiling now
// resolves through — see VendorSplitPolicy.ResolveAuthoredStackSize.
vendor.Apply(
vendorGuid,
new VendorShopProfile(0u, 0u, 0u, false, 1.0f, 1.5f, 0u, 0u, ""),
new[]
{
new VendorShopItem(
arrowsGuid, StackSize: 100, WeenieClassId: 5u, Name: "Arrow",
ItemType: (uint)ItemType.MissileWeapon, IconId: 200u, Value: 100,
DescStackSize: null, PluralName: "Arrows"),
taperGuid, StackSize: -1, WeenieClassId: 5u, Name: "Prismatic Taper",
ItemType: (uint)ItemType.SpellComponents, IconId: 200u, Value: 100,
DescStackSize: null, MaxStackSize: 1000, PluralName: "Prismatic Tapers"),
});
// The item is now materialized (ClientObjectTable.Ingest ran inside
// VendorState.Apply's Changed dispatch) BEFORE selection, exactly
// like a real click on VendorUiController's item list.
Assert.NotNull(objects.Get(arrowsGuid));
Assert.Equal(100, objects.Get(arrowsGuid)!.StackSize);
Assert.NotNull(objects.Get(taperGuid));
Assert.Equal(1000, objects.Get(taperGuid)!.StackSize);
selection.Select(arrowsGuid, SelectionChangeSource.Vendor);
selection.Select(taperGuid, SelectionChangeSource.Vendor);
var slider = Assert.IsType<UiScrollbar>(
layout.FindElement(SelectedObjectController.StackSizeSliderId));
Assert.True(slider.Visible);
Assert.Equal(100u, splitQuantity.Maximum);
Assert.Equal(1000u, splitQuantity.Maximum);
controller.Dispose();
}