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

@ -76,11 +76,13 @@ internal sealed class WorldSelectionQuery
: IWorldSelectionQuery,
IRetainedUiSelectionQuery
{
private const uint LargeUseObjectFlags = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
private const uint StuckObjectFlag = 0x0004u;
/// <summary>
/// ACE's own fallback when a target authors no wire <c>UseRadius</c> at
/// all (<c>WorldObject_Use.cs:50</c>, <c>useRadius ?? 0.6f</c>) — see
/// <see cref="GetUseRadius"/>.
/// </summary>
private const float DefaultUseRadius = 0.6f;
private const float CreatureUseRadius = 3f;
private const float LargeObjectUseRadius = 2f;
private const float AceCanChargeDistance = 7.5f;
private const uint SmallItemMask =
@ -527,13 +529,46 @@ internal sealed class WorldSelectionQuery
ignoreZDelta: false);
}
/// <summary>
/// R2 gate-finding fix (2026-08-08, grand-gate walk-to-vendor
/// regression): retail reads the TARGET's own wire-authored
/// <c>PublicWeenieDesc::_useRadius</c> directly for every
/// range/approach purpose — <c>CPlayerSystem::RegisterObjectRangeHandler</c>
/// (<c>pc:195159</c>/<c>203677</c>/<c>210429</c>; <c>203677</c> is
/// <c>gmVendorUI::OpenVendor</c>'s own range-handler registration,
/// reading <c>eax-&gt;pwd._useRadius</c> for the VENDOR target itself,
/// the exact NPC kind this bug was found on). ACE's server-side
/// acceptance test (<c>WorldObject_Use.cs:47-55</c>,
/// <c>IsWithinUseRadiusOf</c>) reads the SAME wire field:
/// <c>useRadius ?? 0.6f</c> — no item-type special-casing at all.
/// </summary>
/// <remarks>
/// <b>What this replaces.</b> The prior implementation ignored the wire
/// field entirely and guessed a flat radius from the target's item
/// type/flags (3m for ANY <see cref="ItemType.Creature"/>, 2m for a
/// "large object" flag combination, 0.6m otherwise) — an uncited,
/// retail-incorrect heuristic. For a typical vendor NPC (Creature-typed,
/// authoring a much tighter UseRadius than 3m in practice) this made
/// the CLIENT's own local "arrived" test (<c>MoveToManager</c>'s
/// <see cref="AcDream.Core.Physics.Motion.MoveToManager.GetCurrentDistance"/>
/// cylinder-distance arrival check, gated on
/// <see cref="AcDream.Core.Physics.Motion.MovementParameters.DistanceToObject"/>
/// = this method's return value) satisfied well outside ACE's real
/// acceptance zone. The player's walk stopped — and the AP-170/G3
/// arrival-gated Use dispatched — several meters before the player was
/// ever within ACE's own poll-based <c>WithinUseRadius</c> check
/// (<c>Player_Move.cs</c>'s <c>CreateMoveToChain</c>), so
/// <c>ApproachVendor</c> never arrived: the exact "Use lost silently"
/// defect AP-170 closed, reproduced from a different angle (a too-loose
/// LOCAL arrival threshold racing ACE's tighter real one, not a missing
/// arrival gate). The vendor's cosmetic greeting the user observed on
/// approach is a SEPARATE, distance-only proximity emote independent of
/// this Use-radius gate — its firing does not imply the player was ever
/// within ACE's real UseRadius.
/// </remarks>
private float GetUseRadius(uint serverGuid)
{
if ((GetItemType(serverGuid) & ItemType.Creature) != 0)
return CreatureUseRadius;
return _liveEntities.TryGetSnapshot(serverGuid, out var spawn)
&& ((spawn.ObjectDescriptionFlags ?? 0u) & LargeUseObjectFlags) != 0u
? LargeObjectUseRadius
=> _liveEntities.TryGetSnapshot(serverGuid, out var spawn)
&& spawn.UseRadius is > 0f
? spawn.UseRadius.Value
: DefaultUseRadius;
}
}