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

@ -88,13 +88,15 @@ public sealed class WorldSelectionQueryTests
uint objectDescriptionFlags = 0u,
ushort instance = 1,
float scale = 1f,
Quaternion? rotation = null)
Quaternion? rotation = null,
float? useRadius = null)
{
WorldSession.EntitySpawn spawn = Spawn(guid, instance) with
{
ItemType = (uint)type,
Useability = useability,
ObjectDescriptionFlags = objectDescriptionFlags,
UseRadius = useRadius,
};
Runtime.RegisterLiveEntity(spawn);
WorldEntity entity = Runtime.MaterializeLiveEntity(
@ -360,6 +362,71 @@ public sealed class WorldSelectionQueryTests
Assert.Equal(2f, approach.TargetHeight);
}
/// <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 range/approach
/// purposes (<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 a VENDOR target
/// specifically). ACE's server-side acceptance test
/// (<c>WorldObject_Use.cs:47-55</c>, <c>IsWithinUseRadiusOf</c>) reads
/// the SAME wire field, falling back to 0.6f only when the object
/// genuinely doesn't author one.
/// <para>
/// A prior CLIENT-SIDE item-type heuristic (3m for any
/// <see cref="ItemType.Creature"/>, 2m for a "large object" flag
/// combination) ignored the wire field entirely. For a typical vendor
/// NPC (Creature-typed, authoring a much tighter UseRadius than 3m)
/// this made the client's own local "arrived" test satisfied well
/// outside ACE's real acceptance zone: <see cref="AcDream.Core.Physics.Motion.MoveToManager"/>
/// would stop walking and <see cref="SelectionInteractionController.RequestUse"/>
/// would dispatch Use the instant this too-loose local threshold was
/// crossed, long before the player was ever within ACE's own
/// poll-based <c>WithinUseRadius</c> check — reproducing the exact
/// "Use lost silently" defect the AP-170/G3 arrival-gate fix was
/// meant to close, just from a different angle (a too-loose LOCAL
/// arrival threshold, not a missing arrival gate).
/// </para>
/// </summary>
[Fact]
public void ApproachUsesTheCreatureTargetsOwnWireAuthoredUseRadius()
{
var h = new Harness();
// A vendor NPC: Creature-typed, wire-authored UseRadius DIFFERENT
// from both the old 3m heuristic AND the 0.6f no-wire-value
// fallback, so this test actually discriminates "read the wire"
// from either the retired heuristic or a coincidental default match.
h.Add(Target, new Vector3(2f, 0f, 0f), ItemType.Creature, useRadius: 1.5f);
Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach));
Assert.Equal(1.5f, approach.UseRadius);
// 2m > 1.5m: genuinely out of range, must arm a walk — not the old
// heuristic's false "already close enough" (2m <= 3m).
Assert.False(approach.IsCloseRange);
}
/// <summary>
/// Companion to <see cref="ApproachUsesTheCreatureTargetsOwnWireAuthoredUseRadius"/>:
/// when the wire genuinely carries no UseRadius (retail's zeroed-struct
/// default / ACE's <c>wo.UseRadius == null</c> case), the fallback is
/// ACE's own flat 0.6f default (<c>WorldObject_Use.cs:50</c>,
/// <c>useRadius ?? 0.6f</c>) — not a Creature-specific guess.
/// </summary>
[Fact]
public void ApproachFallsBackToAceDefaultUseRadiusWhenTheWireOmitsIt()
{
var h = new Harness();
h.Add(Target, new Vector3(2f, 0f, 0f), ItemType.Creature);
Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach));
Assert.Equal(0.6f, approach.UseRadius);
Assert.False(approach.IsCloseRange);
}
[Fact]
public void PickupAndUseabilityPreserveIndependentRetailGates()
{