fix(vendor): evidence-based pass — max-first stack ceiling; the local player resolves never-animated MoveTo targets
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

Both chains pinned by the live [vendor-diag] run (vendor-diag.log)
after three code-reading rounds each failed:

The split bar: ACE serializes descStackSize=1 for EVERY browse row
(live wire, log 343-348) — the R1-era "ACE never populates desc"
claim is retracted with the line quoted. Retail's vendor sites read
pwd._maxStackSize directly (four sites, incl. UpdateItemsList
@0x004c1ea0 stamping min(remaining, _maxStackSize));
ResolveAuthoredStackSize flips to max-first for its vendor-only
consumers. Taper ceiling 1000, scarab 100, seed 1 for exempt.
Pricing still reads the desc (per-1 values on ACE).

Walk-to-use: the local player's getObjectA seam was bound to
TryGetPhysicsHost, which resolves only INSTALLED physics hosts — a
never-animated vendor has none, so TargetManager.SetTarget got null,
the MoveToObject armed with zero nodes, and UseTime never dispatched.
The log's natural=False completions were the user's own movement keys
(retail-correct input-edge cancels); attempt 4 worked because the
greeting animation had installed a host. RuntimePhysicsState gains
the retail CObjectMaint::GetObjectA seam (bound canonical resolver
with installed-host fallback); the graphical host binds the SAME
lazy-minimal-host resolver every remote already uses — whose own doc
comment names this exact never-animated hazard. The reservation
release was already correct (2b premise refuted with evidence); the
production-wiring invariants are now pinned by four new tests
including the pre-fix pathology as a permanent sabotage control.

AP-169 rewritten a second time, honestly. The [vendor-diag] probe
family (ACDREAM_DUMP_VENDOR) lands env-gated for future live triage.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 17:17:04 +02:00
parent d003449bb4
commit 02b735ba4a
21 changed files with 1139 additions and 102 deletions

View file

@ -136,6 +136,11 @@ public sealed class VendorShopItemMaterializerTests
/// <c>DescStackSize</c>, packed <c>StackSize=-1</c> (unlimited),
/// <c>MaxStackSize=1000</c> -&gt; <c>ClientObject.StackSize</c> resolves
/// to 1000, not 1 and not the (nonsensical, unbounded) packed field.
/// (Second correction, 2026-08-08: the live wire showed ACE actually
/// sends <c>descStackSize=1</c>, so <c>MaxStackSize</c> is now the
/// PRIMARY operand rather than a fallback — see
/// <see cref="Apply_LiveAceWireShape_DescOneMaxHundred_ResolvesToTheAuthoredCeiling"/>;
/// this desc-absent case resolves identically either way.)
/// </summary>
[Fact]
public void Apply_UnlimitedStockNoDescStackSize_FallsBackToMaxStackSize()
@ -157,6 +162,42 @@ public sealed class VendorShopItemMaterializerTests
Assert.Equal(1000, item.StackSizeMax);
}
/// <summary>
/// 2026-08-08 live-evidence re-fix (register AP-169, second
/// correction): the EXACT wire shape the vendor-diag run captured from
/// the live ACE server — `descStackSize=1 stackSizeMax=100` for every
/// browse row (e.g. the Smelting Pot / Lead Scarab rows,
/// `[vendor-diag] ApproachVendor wire-item[0] ... descStackSize=1
/// stackSizeMax=100`). ACE DOES serialize the instance stack size, at
/// the useless value 1, so the R1 desc-first preference resolved every
/// vendor stack to 1 and the toolbar split slider never appeared
/// (`ApplySelection ... failingPredicate=stackSize&lt;=1u`). Retail's
/// own vendor UI reads <c>pwd._maxStackSize</c> directly
/// (<c>VendorItemsUI::UpdateItemsList</c> <c>0x004c1ea0</c>,
/// <c>pc:201085-201133</c>), so the materialized ceiling must be 100
/// here. Sabotage-verified: restoring the desc-first preference makes
/// this resolve 1 and fail.
/// </summary>
[Fact]
public void Apply_LiveAceWireShape_DescOneMaxHundred_ResolvesToTheAuthoredCeiling()
{
var vendor = new VendorState();
var objects = new ClientObjectTable();
using var materializer = new VendorShopItemMaterializer(vendor, objects);
vendor.Apply(VendorGuid, default, new[]
{
new VendorShopItem(
ItemA, StackSize: -1, WeenieClassId: 1u, Name: "Lead Scarab",
ItemType: (uint)ItemType.SpellComponents, IconId: 0x1234u, Value: 10,
DescStackSize: 1, MaxStackSize: 100),
});
ClientObject item = objects.Get(ItemA)!;
Assert.Equal(100, item.StackSize);
Assert.Equal(100, item.StackSizeMax);
}
/// <summary>
/// Sabotage-adjacent control: the SAME unlimited-stock listing but with
/// <see cref="VendorShopItem.MaxStackSize"/> ALSO absent (neither wire