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>
251 lines
8.9 KiB
C#
251 lines
8.9 KiB
C#
using AcDream.Core.Items;
|
|
|
|
namespace AcDream.Core.Tests.Items;
|
|
|
|
/// <summary>
|
|
/// Conformance tests for <see cref="VendorSellAcceptability"/> — the port of
|
|
/// <c>VendorSellUI::DragItemAcceptable</c> (<c>pc:201195-201307</c>) chained
|
|
/// into <c>VendorProfile::InqAcceptability</c> (<c>pc:484768-484797</c>).
|
|
/// </summary>
|
|
public sealed class VendorSellAcceptabilityTests
|
|
{
|
|
private const uint Armor = (uint)ItemType.Armor;
|
|
private const uint Weapon = (uint)ItemType.Weapon;
|
|
private const uint NoLimit = VendorSellAcceptability.NoLimit;
|
|
|
|
[Fact]
|
|
public void NotOwnedByPlayerIsRejectedBeforeAnyOtherCheck()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: false,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 100,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
Assert.Equal(VendorSellRejection.NotOwnedByPlayer, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void ANonEmptyContainerBypassesTheTypeAndValueFilterEntirely()
|
|
{
|
|
// pc:201229-201233: GetNumContainedItems > 0 -> accept unconditionally,
|
|
// even though the container's OWN type (Weapon) does not intersect
|
|
// the vendor's merchandise mask (Armor) and it has zero value.
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 3,
|
|
itemTypeMask: Weapon,
|
|
perUnitValue: 0,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
Assert.Equal(VendorSellRejection.None, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void AWrongItemTypeIsRejected()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Weapon,
|
|
perUnitValue: 100,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
Assert.Equal(VendorSellRejection.WrongType, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroPerUnitValueIsRejectedAsNoValue()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 0,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
Assert.Equal(VendorSellRejection.NoValue, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void AboveTheAuthoredMaxValueIsRejectedAsTooValuable()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 1001,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: 1000u);
|
|
|
|
Assert.Equal(VendorSellRejection.TooValuable, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExactlyAtTheAuthoredMaxValueIsAcceptable()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 1000,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 0u,
|
|
merchandiseMaxValue: 1000u);
|
|
|
|
Assert.Equal(VendorSellRejection.None, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void BelowTheAuthoredMinValueIsRejectedAsTooCheap()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 4,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 5u,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
Assert.Equal(VendorSellRejection.TooCheap, rejection);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoLimitSentinelDisablesBothMaxAndMinChecks()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: int.MaxValue - 1,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: NoLimit,
|
|
merchandiseMaxValue: NoLimit);
|
|
|
|
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()
|
|
{
|
|
VendorSellRejection rejection = VendorSellAcceptability.Evaluate(
|
|
ownedByPlayer: true,
|
|
containedItemCount: 0,
|
|
itemTypeMask: Armor,
|
|
perUnitValue: 500,
|
|
merchandiseItemTypes: Armor,
|
|
merchandiseMinValue: 1u,
|
|
merchandiseMaxValue: 10_000u);
|
|
|
|
Assert.Equal(VendorSellRejection.None, rejection);
|
|
}
|
|
|
|
// ---- MessageFor: exact retail strings, recovered from the decompiled ----
|
|
// ---- binary's data segment (see VendorSellAcceptability's doc comment). ----
|
|
|
|
[Theory]
|
|
[InlineData(VendorSellRejection.NotOwnedByPlayer, "You can only sell items you are carrying")]
|
|
[InlineData(VendorSellRejection.CannotBeSoldHere, "That item cannot be sold here")]
|
|
[InlineData(VendorSellRejection.NoValue, "That item has no value and cannot be sold")]
|
|
[InlineData(VendorSellRejection.TooCheap, "That item is too cheap to sell here")]
|
|
[InlineData(VendorSellRejection.TooValuable, "That item is too valuable to sell here")]
|
|
[InlineData(VendorSellRejection.WrongType, "You cannot sell that here")]
|
|
public void MessageForReturnsRetailsExactString(VendorSellRejection rejection, string expected)
|
|
{
|
|
Assert.Equal(expected, VendorSellAcceptability.MessageFor(rejection));
|
|
}
|
|
|
|
[Fact]
|
|
public void MessageForAcceptableReturnsNull()
|
|
{
|
|
Assert.Null(VendorSellAcceptability.MessageFor(VendorSellRejection.None));
|
|
}
|
|
}
|