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
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:
parent
d003449bb4
commit
02b735ba4a
21 changed files with 1139 additions and 102 deletions
27
src/AcDream.Core/Items/VendorDiagnostics.cs
Normal file
27
src/AcDream.Core/Items/VendorDiagnostics.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Items;
|
||||
|
||||
/// <summary>
|
||||
/// TEMPORARY diagnostic-probe owner for two live-only regressions that have
|
||||
/// each survived two green-tested fixes: Chain A (a far-click walk-to-use
|
||||
/// approaches a vendor, the vendor plays its cosmetic greeting, but the shop
|
||||
/// window never opens) and Chain B (selecting a splittable vendor stack
|
||||
/// shows the bare item name with no quantity slider). Every probe line is
|
||||
/// prefixed <c>[vendor-diag]</c> and gated on <see cref="DumpVendorEnabled"/>
|
||||
/// so the family is a true no-op when the flag is unset.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Read once from <c>ACDREAM_DUMP_VENDOR=1</c> at process start, per Code
|
||||
/// Structure Rule 5 (one static diagnostic-owner class per subsystem, no
|
||||
/// per-call-site <see cref="Environment.GetEnvironmentVariable"/> reads).
|
||||
/// Lives in <c>AcDream.Core</c> — the one project every call site (App,
|
||||
/// Core.Net, Runtime) already references — so a single flag instance is
|
||||
/// shared across the whole probe family regardless of which layer observes
|
||||
/// it first.
|
||||
/// </remarks>
|
||||
public static class VendorDiagnostics
|
||||
{
|
||||
public static bool DumpVendorEnabled { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_VENDOR") == "1";
|
||||
}
|
||||
|
|
@ -44,35 +44,57 @@ public static class VendorSplitPolicy
|
|||
: authoredStackSize is { } size && size > 0 ? size : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Grand-gate finding R1 (2026-08-08, register AP-169 correction): the
|
||||
/// retail-faithful "how big is one stack of this item" answer for a
|
||||
/// VENDOR-owned selection, used everywhere retail reads
|
||||
/// <c>PublicWeenieDesc::_stackSize</c> for splitting/seeding purposes
|
||||
/// (<c>gmToolbarUI::HandleSelectionChanged</c>,
|
||||
/// <c>pc:198688</c>/<c>198744</c>/<c>198774</c>/<c>198791</c>;
|
||||
/// <c>ItemHolder::GetObjectSplitSize</c>, <c>pc:401465-401477</c>).
|
||||
/// Grand-gate finding R1, CORRECTED at the 2026-08-08 live-evidence
|
||||
/// re-fix (register AP-169, second correction): the retail-faithful
|
||||
/// "how big is one stack of this item" quantity operand for a
|
||||
/// VENDOR-owned selection/listing.
|
||||
///
|
||||
/// <para>
|
||||
/// Prefers <paramref name="descStackSize"/> (the wire's own
|
||||
/// <c>PublicWeenieDesc::_stackSize</c> — retail-faithful FIRST, honored
|
||||
/// unchanged if a real retail server or a future ACE fix ever populates
|
||||
/// it). ACE never sets it for a vendor browse-list row
|
||||
/// (<c>Vendor.LoadInventoryItem</c> never calls <c>wo.SetStackSize</c>),
|
||||
/// so this falls back to <paramref name="maxStackSize"/> — the item
|
||||
/// TYPE's authored stack ceiling, which ACE DOES populate (an ordinary
|
||||
/// weenie property, not an instance-specific stack count) and which a
|
||||
/// real retail server evidently uses AS <c>_stackSize</c> for an
|
||||
/// unlimited-supply "one full stack" browse listing (the live retail
|
||||
/// screenshot: a Prismatic Taper listing shows "1000 Prismatic Tapers",
|
||||
/// 1000 being the taper's authored max stack size, not any bounded
|
||||
/// supply count). Finally falls back to 1 (non-splittable) when neither
|
||||
/// field is available. See the register, AP-169.
|
||||
/// <b>The operand is <paramref name="maxStackSize"/> FIRST.</b> Retail's
|
||||
/// own vendor-owned quantity sites read
|
||||
/// <c>PublicWeenieDesc::_maxStackSize</c> DIRECTLY, never the instance
|
||||
/// <c>_stackSize</c>: <c>VendorItemsUI::UpdateItemsList</c>
|
||||
/// (<c>0x004c1ea0</c>, <c>pc:201085-201133</c>) displays each browse
|
||||
/// row's quantity as <c>pwd._maxStackSize</c> (clamped by the packed
|
||||
/// supply count only when that count is finite —
|
||||
/// <c>SetObjectStackSize(this, obj, min(remaining, _maxStackSize))</c>;
|
||||
/// an unlimited <c>0xffffffff</c> listing shows plain
|
||||
/// <c>_maxStackSize</c>); <c>gmVendorUI::InqListSlotCount</c>
|
||||
/// (<c>0x004c0c10</c>, <c>pc:200052</c>) classifies list rows on
|
||||
/// <c>pwd._maxStackSize <= 1</c>; and the Buy button cases
|
||||
/// (<c>gmVendorUI::HandleButtonClicks</c> cases <c>0x100000c9</c> at
|
||||
/// <c>pc:203996</c> / <c>0x100000cb</c> at <c>pc:204086</c>) gate the
|
||||
/// stackable-buy path on <c>pwd._maxStackSize > 1</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Why desc-first was wrong (the live evidence).</b> The R1 version
|
||||
/// of this method preferred <paramref name="descStackSize"/> on the
|
||||
/// theory that ACE never populates it for a browse row. The 2026-08-08
|
||||
/// vendor-diag live run refuted that: ACE serializes
|
||||
/// <c>descStackSize=1</c> for EVERY browse row (`[vendor-diag]
|
||||
/// ApproachVendor wire-item[...] descStackSize=1 stackSizeMax=100`), so
|
||||
/// desc-first resolved every vendor stack to 1 and the toolbar split
|
||||
/// slider never appeared (`ApplySelection ... failingPredicate=
|
||||
/// stackSize<=1u stackSize=1`). Max-first matches both retail's own
|
||||
/// operand and the live retail screenshot ("1000 Prismatic Tapers",
|
||||
/// ceiling 1000 = the taper's authored max stack size).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="descStackSize"/> remains the fallback when no
|
||||
/// authored ceiling exists, then 1 (non-splittable). This helper is
|
||||
/// consumed ONLY by the vendor-owned paths
|
||||
/// (<c>VendorShopItemMaterializer.ToWeenieData</c> and
|
||||
/// <c>VendorUiController.ResolveBuyQuantity</c>); player-inventory
|
||||
/// stacks read their own wire <c>ClientObject.StackSize</c> and are
|
||||
/// deliberately untouched by this rule. See the register, AP-169.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static int ResolveAuthoredStackSize(int? descStackSize, int? maxStackSize) =>
|
||||
descStackSize is { } desc && desc > 0
|
||||
? desc
|
||||
: maxStackSize is { } max && max > 0
|
||||
? max
|
||||
maxStackSize is { } max && max > 0
|
||||
? max
|
||||
: descStackSize is { } desc && desc > 0
|
||||
? desc
|
||||
: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,22 +58,21 @@ public readonly record struct VendorShopItem(
|
|||
// matching retail's own zeroed-struct default of 0 for the same case
|
||||
// (see VendorPricing.PerUnitValue's <= 0 guard).
|
||||
int? DescStackSize = null,
|
||||
// Grand-gate finding R1 (2026-08-08, register AP-169 correction): the
|
||||
// item TYPE's authored stack ceiling (retail PublicWeenieDesc::
|
||||
// _maxStackSize, wire AcDream.Core.Net.Messages.PublicWeenieDescBody.
|
||||
// StackSizeMax). Retail's OWN InqListSlotCount (pc:200038-200065) and
|
||||
// the Buying tab's Buy Item/Clear Item removal rule
|
||||
// Grand-gate finding R1, corrected at the 2026-08-08 live-evidence
|
||||
// re-fix (register AP-169, second correction): the item TYPE's authored
|
||||
// stack ceiling (retail PublicWeenieDesc::_maxStackSize, wire
|
||||
// AcDream.Core.Net.Messages.PublicWeenieDescBody.StackSizeMax). This is
|
||||
// retail's PRIMARY vendor-owned quantity operand, read literally by
|
||||
// VendorItemsUI::UpdateItemsList (0x004c1ea0, pc:201085-201133 — each
|
||||
// browse row displays min(remaining, _maxStackSize), plain
|
||||
// _maxStackSize for unlimited supply), InqListSlotCount (pc:200052),
|
||||
// and the Buying tab's Buy Item/Clear Item removal rule
|
||||
// (gmVendorUI::HandleButtonClicks cases 0x100000c9/0x100000cb,
|
||||
// pc:203989-204010/204080-204094) read THIS field literally, never
|
||||
// DescStackSize, to decide whether an item is stackable at all. It also
|
||||
// serves as this port's retail-faithful SUBSTITUTE for the toolbar's
|
||||
// splitSize/maxSplitSize seed (gmToolbarUI::HandleSelectionChanged,
|
||||
// pc:198688/198744/198774/198791 — reads PublicWeenieDesc::_stackSize,
|
||||
// which ACE never populates for a vendor browse-list row; a real retail
|
||||
// server evidently authors _stackSize == _maxStackSize for an
|
||||
// unlimited-supply "one full stack" browse listing, so MaxStackSize is
|
||||
// the value retail's own server would have put there) — see
|
||||
// VendorSplitPolicy.ResolveAuthoredStackSize and the register (AP-169).
|
||||
// pc:203996/204086) — never the instance _stackSize. The live ACE wire
|
||||
// (vendor-diag 2026-08-08) carries descStackSize=1 for every browse
|
||||
// row, so any desc-first read collapses every vendor stack to 1 — see
|
||||
// VendorSplitPolicy.ResolveAuthoredStackSize (max-first, desc
|
||||
// fallback) and the register (AP-169).
|
||||
int? MaxStackSize = null,
|
||||
// Review finding F5 (Slice 5.4 review): PublicWeenieDescBody already
|
||||
// carries these three (IconOverlayId/IconUnderlayId/UiEffects) — see
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue