namespace AcDream.Core.Items;
///
/// Outcome of — retail
/// VendorSellUI::DragItemAcceptable's (pc:201195-201307,
/// 0x004c20c0) full gate chain: ownership, the non-empty-container
/// bypass, then VendorProfile::InqAcceptability
/// (pc:484768-484797, 0x005d1a90).
///
public enum VendorSellRejection
{
/// Acceptable — stage the drop.
None = 0,
/// ACCWeenieObject::IsOwnedByPlayer(esi) == 0 (pc:201206).
NotOwnedByPlayer,
///
/// InqAcceptability returned the raw item_types bitmask
/// (the type-mismatch/non-sellable-bit branch, pc:005d1af8) —
/// mapped here to the generic "You cannot sell that here" message the
/// same way retail's DragItemAcceptable switch falls through for
/// any return value outside its four named cases (1-4).
///
/// F12 (Slice 6b/6c review) — SOFTENED CLAIM: an earlier version of this
/// doc comment asserted a genuine bitmask is "almost never literally
/// 1-4." That is not true in general:
/// MeleeWeapon/Armor/Clothing are the single bits
/// 1/2/4, and a real specialist vendor's own
/// MerchandiseItemTypes could legitimately be exactly one of
/// them (a weapon-only, armor-only, or clothing-only shop is an
/// ordinary AC vendor archetype). For such a vendor, retail's raw
/// item_types return WOULD collide with DragItemAcceptable's
/// own named cases 1/2/4 ("cannot be sold here" / "has no value" /
/// "too valuable") — a genuine type mismatch would show the WRONG
/// retail message, not the generic one. This class does not reproduce
/// that collision (it always returns the semantic
/// case, never a raw integer another case could
/// alias), so acdream's own message is unaffected either way; this note
/// only corrects the doc's claim about how often retail's OWN collision
/// is reachable, in case a byte-exact reproduction is ever wanted.
///
///
WrongType,
///
/// InqAcceptability literally returned 1 — retail's
/// DragItemAcceptable switch has a case for it
/// (pc:201259-201267), but nothing in InqAcceptability's
/// own body ever produces a literal 1 (only 0, 2, 3, the "too valuable"
/// value, or the raw type bitmask) — ported for exact control-flow
/// fidelity per CLAUDE.md's "do not simplify the switch" rule, not
/// because it is known to be reachable.
///
CannotBeSoldHere,
/// InqAcceptability == 2: per-unit value is exactly zero (pc:005d1ac3).
NoValue,
///
/// InqAcceptability's "too valuable" branch (pc:005d1add):
/// max_value != -1 && value > max_value.
///
/// F3 (Slice 6b/6c review, byte-verified at 0x005d1add): the
/// actual x86 at that return site is mov eax,edi; shr eax,0x10;
/// not eax; and eax,4; ret — i.e. (~(itemTypeMask >> 16)) & 4,
/// a BITWISE complement (the decompiled pseudo-C's ! is
/// misleading — it is not a logical NOT). ItemType.PromissoryNote
/// (0x00040000, bit 18) sits exactly at bit 2 of
/// itemTypeMask >> 16, so a trade note above the vendor's
/// max value returns 0 (fully , exempt) instead of 4
/// () — and the ret at that exact
/// address means the min-value check below is skipped entirely for a
/// trade note, not merely the max-value rejection.
///
///
TooValuable,
///
/// InqAcceptability == 3: min_value != -1 && value < min_value
/// (pc:005d1af2).
///
TooCheap,
}
///
/// Pure port of VendorSellUI::DragItemAcceptable +
/// VendorProfile::InqAcceptability — gates a Selling-tab drop AND its
/// hover-preview cursor (the silent distinction is the caller's job:
/// always computes the same outcome, callers choose
/// whether to surface ).
///
public static class VendorSellAcceptability
{
/// Retail's -1/0xffffffff "no limit" sentinel for min_value/max_value.
public const uint NoLimit = uint.MaxValue;
/// ACCWeenieObject::IsOwnedByPlayer(esi).
///
/// ACCWeenieObject::GetNumContainedItems(esi) — a non-empty
/// container (a bag with stuff in it) always passes, bypassing the
/// type/value filter entirely (pc:201229-201233).
///
/// The dragged item's own PublicWeenieDesc::_type.
///
/// The dragged item's per-unit value —
/// applied to its own Value/StackSize, matching
/// InqAcceptability's own _stackSize > 0 ? _value/_stackSize : _value
/// division (pc:005d1ab2-005d1ab6).
///
/// The vendor's VendorShopProfile.MerchandiseItemTypes.
///
/// The vendor's VendorShopProfile.MerchandiseMinValue —
/// means retail's unset -1.
///
///
/// The vendor's VendorShopProfile.MerchandiseMaxValue —
/// means retail's unset -1.
///
///
/// F4 (Slice 6b/6c review): the dragged item's own
/// PublicWeenieDesc::_bitfield (,
/// populated on every ordinary CreateObject — including the
/// player's own pack items, the only things ever dragged here). Tested
/// against
/// (BF_RETAINED = 0x01000000, acclient.h:6456) — retail's
/// InqAcceptability ORs this bit into the SAME type-mismatch
/// branch (byte 3 bit 0 of the bitfield, pc:005d1aa7), so it
/// folds into the same
/// outcome, not a distinct rejection reason.
///
public static VendorSellRejection Evaluate(
bool ownedByPlayer,
int containedItemCount,
uint itemTypeMask,
int perUnitValue,
uint merchandiseItemTypes,
uint merchandiseMinValue,
uint merchandiseMaxValue,
uint publicWeenieBitfield = 0u)
{
if (!ownedByPlayer)
return VendorSellRejection.NotOwnedByPlayer;
if (containedItemCount > 0)
return VendorSellRejection.None;
// F4: InqAcceptability's first check ORs the type-mask mismatch
// with the BF_RETAINED bit (pc:005d1aa7) -- both branches return
// the SAME raw item_types value, so both fold into WrongType here.
bool retained = (publicWeenieBitfield & (uint)PublicWeenieFlags.Retained) != 0u;
if ((itemTypeMask & merchandiseItemTypes) == 0u || retained)
return VendorSellRejection.WrongType;
if (perUnitValue == 0)
return VendorSellRejection.NoValue;
if (merchandiseMaxValue != NoLimit && perUnitValue > merchandiseMaxValue)
{
// F3 (byte-verified at 0x005d1add) -- see TooValuable's own doc
// comment: a PromissoryNote (trade note) is EXEMPT from the
// max-value rejection (and, by the disassembly's early ret,
// from the min-value check too), not just capped differently.
return (itemTypeMask & (uint)ItemType.PromissoryNote) != 0u
? VendorSellRejection.None
: VendorSellRejection.TooValuable;
}
if (merchandiseMinValue != NoLimit && perUnitValue < merchandiseMinValue)
return VendorSellRejection.TooCheap;
return VendorSellRejection.None;
}
///
/// Retail's exact rejection strings, read directly out of the decompiled
/// binary's data segment (docs/research/named-retail/acclient_2013_pseudo_c.txt,
/// addresses 0x007b51a8/0x007b51e0/0x007b5230/
/// 0x007b5278/0x007b52cc/0x007b5308) resolving the
/// truncated "…" citations the Slice 6b/6c research doc quoted. Delivered
/// via ECM_UI::SendNotice_DisplayStringInfo(0x1a, ...) — the SAME
/// system-message notice channel (0x1a) every other retail-ported
/// transient string in this codebase already uses.
///
public static string? MessageFor(VendorSellRejection rejection) => rejection switch
{
VendorSellRejection.None => null,
VendorSellRejection.NotOwnedByPlayer => "You can only sell items you are carrying",
VendorSellRejection.CannotBeSoldHere => "That item cannot be sold here",
VendorSellRejection.NoValue => "That item has no value and cannot be sold",
VendorSellRejection.TooCheap => "That item is too cheap to sell here",
VendorSellRejection.TooValuable => "That item is too valuable to sell here",
VendorSellRejection.WrongType => "You cannot sell that here",
_ => "You cannot sell that here",
};
}