fix(vendor): 6b/6c review corrections — pre-send guards, accumulating staging, trade-note exemption, drag-over tab switch, full-stack sells
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
All thirteen findings, each anchored in recovered bytes or pc reads:
Buy All now runs retail's four PRE-SEND guards in order (pyreal and
alt-currency affordability, container and item slot capacity; strings
recovered from .rdata at 0x007b57b4/0x007b5750) — a rejected batch can
no longer destroy the staged list. Staged adds ACCUMULATE with the
5000 cap ("I can't possibly sell you that much!..." @0x007b59d8) and
the shop rows decrement/restore per RemoveFromShop. The max-value sell
rejection exempts trade notes — the raw bytes at 0x005d1add are `not`
(bitwise), not the pseudo-C's misleading `!`, and the early ret skips
the min check too. BF_RETAINED gates selling end to end (the bit was
already on ClientObject; AP-164's three claims were all false once
traced — RETIRED). Dragging over the vendor window auto-opens the
Selling tab per UpdateDragOver — with a correction to the review's own
citation: token 0x100000cd is the SELLING page, the guard is
"don't reopen the current tab." Sells are full-stack-only (three
retail sites; "Cannot sell part of a stack" @0x007b57ec) and Sell Item
acts on the global selection unconditionally. The confirm string gains
its byte-true trailing '?', dies with the session, staged-row
highlights repaint, dead guids unstage with retail's shopping-list
notice, and move-to-use no longer walks to targets the dispatch would
refuse.
AP-162 narrowed, AP-164 retired, AP-167/AP-168 filed honest.
Clean-room complete solution: 11,508 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
92ea3977b6
commit
c68ad1e646
11 changed files with 1314 additions and 99 deletions
|
|
@ -136,6 +136,83 @@ public sealed class VendorSellAcceptabilityTests
|
|||
Assert.Equal(VendorSellRejection.None, rejection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F3 (Slice 6b/6c review, byte-verified at 0x005d1add): a trade note
|
||||
/// above the vendor's max value is EXEMPT — the actual x86 is
|
||||
/// <c>(~(itemTypeMask >> 16)) & 4</c>, and PromissoryNote's bit
|
||||
/// (0x00040000) lands exactly on that mask, zeroing the result.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PromissoryNoteAboveMaxValueIsExemptFromTooValuable()
|
||||
{
|
||||
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
||||
ownedByPlayer: true,
|
||||
containedItemCount: 0,
|
||||
itemTypeMask: (uint)ItemType.PromissoryNote,
|
||||
perUnitValue: 999_999,
|
||||
merchandiseItemTypes: (uint)ItemType.PromissoryNote,
|
||||
merchandiseMinValue: 0u,
|
||||
merchandiseMaxValue: 1000u);
|
||||
|
||||
Assert.Equal(VendorSellRejection.None, rejection);
|
||||
}
|
||||
|
||||
/// <summary>An ordinary (non-note) item above max value is still rejected — the exemption is note-specific.</summary>
|
||||
[Fact]
|
||||
public void NonPromissoryNoteAboveMaxValueIsStillTooValuable()
|
||||
{
|
||||
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
||||
ownedByPlayer: true,
|
||||
containedItemCount: 0,
|
||||
itemTypeMask: Armor,
|
||||
perUnitValue: 999_999,
|
||||
merchandiseItemTypes: Armor,
|
||||
merchandiseMinValue: 0u,
|
||||
merchandiseMaxValue: 1000u);
|
||||
|
||||
Assert.Equal(VendorSellRejection.TooValuable, rejection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F4 (Slice 6b/6c review): <c>BF_RETAINED</c>
|
||||
/// (<see cref="PublicWeenieFlags.Retained"/>, 0x01000000) folds into
|
||||
/// the SAME <see cref="VendorSellRejection.WrongType"/> outcome as a
|
||||
/// genuine type mismatch — retail's InqAcceptability ORs the two
|
||||
/// conditions together (pc:005d1aa7).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RetainedItemIsRejectedAsWrongTypeEvenWhenTheTypeMaskMatches()
|
||||
{
|
||||
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
||||
ownedByPlayer: true,
|
||||
containedItemCount: 0,
|
||||
itemTypeMask: Armor,
|
||||
perUnitValue: 100,
|
||||
merchandiseItemTypes: Armor,
|
||||
merchandiseMinValue: 0u,
|
||||
merchandiseMaxValue: NoLimit,
|
||||
publicWeenieBitfield: (uint)PublicWeenieFlags.Retained);
|
||||
|
||||
Assert.Equal(VendorSellRejection.WrongType, rejection);
|
||||
}
|
||||
|
||||
/// <summary>An item with OTHER bitfield bits set (not Retained) is unaffected.</summary>
|
||||
[Fact]
|
||||
public void NonRetainedBitfieldDoesNotAffectAcceptability()
|
||||
{
|
||||
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
||||
ownedByPlayer: true,
|
||||
containedItemCount: 0,
|
||||
itemTypeMask: Armor,
|
||||
perUnitValue: 100,
|
||||
merchandiseItemTypes: Armor,
|
||||
merchandiseMinValue: 0u,
|
||||
merchandiseMaxValue: NoLimit,
|
||||
publicWeenieBitfield: (uint)PublicWeenieFlags.Stuck);
|
||||
|
||||
Assert.Equal(VendorSellRejection.None, rejection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnOrdinaryAcceptableItemReturnsNone()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,8 +26,14 @@ public sealed class VendorStagingListTests
|
|||
Assert.False(list.IsEmpty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2 (Slice 6b/6c review, byte-verified <c>pc:202934</c>): re-adding an
|
||||
/// already-staged guid ACCUMULATES onto the existing quantity — a prior
|
||||
/// version of this port upserted/overwrote instead, which this test
|
||||
/// used to assert (20, not 25).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddingTheSameGuidTwiceUpsertsRatherThanDuplicating()
|
||||
public void AddingTheSameGuidTwiceAccumulatesRatherThanDuplicatingOrOverwriting()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
|
||||
|
|
@ -35,7 +41,64 @@ public sealed class VendorStagingListTests
|
|||
list.Add(ItemA, 20);
|
||||
|
||||
VendorStagingEntry entry = Assert.Single(list.Entries);
|
||||
Assert.Equal(20, entry.Quantity);
|
||||
Assert.Equal(25, entry.Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddReturnsAddedOnASuccessfulStage()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
|
||||
Assert.Equal(VendorStagingAddOutcome.Added, list.Add(ItemA, 5));
|
||||
Assert.Equal(VendorStagingAddOutcome.Added, list.Add(ItemA, 5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: retail's 5000-unit cap (<c>0x1388</c>) guards the ACCUMULATE
|
||||
/// path only — exceeding it on a re-add rejects and leaves the entry
|
||||
/// COMPLETELY UNCHANGED (<c>pc:202936-202951</c>).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddAccumulatingPastTheCapIsRejectedAndLeavesTheEntryUnchanged()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
list.Add(ItemA, VendorStagingList.MaxStagedQuantity - 10);
|
||||
|
||||
VendorStagingAddOutcome outcome = list.Add(ItemA, 11);
|
||||
|
||||
Assert.Equal(VendorStagingAddOutcome.Capped, outcome);
|
||||
VendorStagingEntry entry = Assert.Single(list.Entries);
|
||||
Assert.Equal(VendorStagingList.MaxStagedQuantity - 10, entry.Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAccumulatingExactlyToTheCapSucceeds()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
list.Add(ItemA, VendorStagingList.MaxStagedQuantity - 10);
|
||||
|
||||
VendorStagingAddOutcome outcome = list.Add(ItemA, 10);
|
||||
|
||||
Assert.Equal(VendorStagingAddOutcome.Added, outcome);
|
||||
VendorStagingEntry entry = Assert.Single(list.Entries);
|
||||
Assert.Equal(VendorStagingList.MaxStagedQuantity, entry.Quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: a BRAND-NEW entry has no cap in retail's own <c>AddToBuyList</c>
|
||||
/// — the cap check lives only inside the "found an existing match"
|
||||
/// branch (<c>label_4c3e20</c>'s insert has none, <c>pc:202895-202923</c>).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddOfABrandNewEntryHasNoCapEvenAboveTheThreshold()
|
||||
{
|
||||
var list = new VendorStagingList();
|
||||
|
||||
VendorStagingAddOutcome outcome = list.Add(ItemA, VendorStagingList.MaxStagedQuantity + 500);
|
||||
|
||||
Assert.Equal(VendorStagingAddOutcome.Added, outcome);
|
||||
VendorStagingEntry entry = Assert.Single(list.Entries);
|
||||
Assert.Equal(VendorStagingList.MaxStagedQuantity + 500, entry.Quantity);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -46,8 +109,9 @@ public sealed class VendorStagingListTests
|
|||
{
|
||||
var list = new VendorStagingList();
|
||||
|
||||
list.Add(guid, quantity);
|
||||
VendorStagingAddOutcome outcome = list.Add(guid, quantity);
|
||||
|
||||
Assert.Equal(VendorStagingAddOutcome.Ignored, outcome);
|
||||
Assert.True(list.IsEmpty);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue