using AcDream.Core.Items;
namespace AcDream.Core.Tests.Items;
///
/// Conformance tests for — the port of
/// VendorSellUI::DragItemAcceptable (pc:201195-201307) chained
/// into VendorProfile::InqAcceptability (pc:484768-484797).
///
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);
}
///
/// F3 (Slice 6b/6c review, byte-verified at 0x005d1add): a trade note
/// above the vendor's max value is EXEMPT — the actual x86 is
/// (~(itemTypeMask >> 16)) & 4, and PromissoryNote's bit
/// (0x00040000) lands exactly on that mask, zeroing the result.
///
[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);
}
/// An ordinary (non-note) item above max value is still rejected — the exemption is note-specific.
[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);
}
///
/// F4 (Slice 6b/6c review): BF_RETAINED
/// (, 0x01000000) folds into
/// the SAME outcome as a
/// genuine type mismatch — retail's InqAcceptability ORs the two
/// conditions together (pc:005d1aa7).
///
[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);
}
/// An item with OTHER bitfield bits set (not Retained) is unaffected.
[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));
}
}