fix: a trade offer takes no undeletable item and no unapproved model

The client's trade window also refuses an item that can not be
deleted and a brick built model whose moderation status is not
approved. An offer with such an item is now refused like the other
offers the window can not make.

Whether an item can be deleted is what the inventory already works
out for a delete: not the item of the mount the player rides, and not
an item its row of the DeletionRestrictions table restricts.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 16:06:31 -06:00
parent d5723696f4
commit ba0c5756ad
3 changed files with 72 additions and 3 deletions

View File

@@ -43,7 +43,7 @@ std::optional<std::vector<TradeItem>> Trade::ResolveOffer(
if (tradeItem.itemCount == 0 || !seen.insert(tradeItem.itemId).second) return std::nullopt;
const auto held = findHeld(tradeItem.itemId);
if (!held || held->count == 0 || held->bound || held->equipped) return std::nullopt;
if (!held || held->count == 0 || held->bound || held->equipped || !held->deletable || !held->approved) return std::nullopt;
resolved.push_back({ tradeItem.itemId, held->lot, std::min(tradeItem.itemCount, held->count) });
}
@@ -55,10 +55,22 @@ std::function<std::optional<HeldTradeItem>(LWOOBJID)> Trade::HeldIn(const Invent
return [&inventory](const LWOOBJID itemId) -> std::optional<HeldTradeItem> {
const auto* const item = inventory.FindItemById(itemId);
if (!item) return std::nullopt;
return HeldTradeItem{ item->GetLot(), item->GetCount(), item->GetBound(), item->IsEquipped() };
return HeldTradeItem{
item->GetLot(), item->GetCount(), item->GetBound(), item->IsEquipped(),
inventory.CanDelete(*item), IsApproved(item->GetLot(), item->GetConfig())
};
};
}
bool Trade::IsApproved(const LOT lot, const LwoNameValue& config) {
if (lot != BrickBuiltModelLot) return true;
const auto status = config.find(u"userModelMod");
if (status == config.end() || status->second->GetValueType() != eLDFType::LDF_TYPE_S32) return false;
return static_cast<const LDFData<int32_t>*>(status->second.get())->GetValue() == ApprovedModerationStatus;
}
Trade::~Trade() {
}

View File

@@ -23,8 +23,13 @@ struct HeldTradeItem {
// The trade window takes neither a bound item nor an equipped one
bool bound = false;
bool equipped = false;
// Nor an item the player may not delete, nor a brick built model that is not approved
bool deletable = true;
bool approved = true;
};
struct LwoNameValue;
class InventoryComponent;
class Trade {
@@ -35,7 +40,8 @@ public:
* @param findHeld gives the held item with an ID, or nothing if the player has no such item
* @return the offer with every LOT taken from the held item and every count limited to the held count,
* or nothing if the offer names an item the player does not hold, the same item twice, a count of 0,
* a bound or equipped item, or more items than the trade window has slots
* a bound or equipped item, an item the player may not delete, a brick built model that is not approved,
* or more items than the trade window has slots
*/
static std::optional<std::vector<TradeItem>> ResolveOffer(
const std::vector<TradeItem>& offered,
@@ -46,6 +52,18 @@ public:
*/
static std::function<std::optional<HeldTradeItem>(LWOOBJID)> HeldIn(const InventoryComponent& inventory);
/**
* Whether the trade window takes an item for its moderation status. Only a brick built model has one,
* the userModelMod of its config, and only an approved one is taken.
*/
static bool IsApproved(LOT lot, const LwoNameValue& config);
// The item a brick built model is
static constexpr LOT BrickBuiltModelLot = 6662;
// The userModelMod of an approved model
static constexpr int32_t ApprovedModerationStatus = 1;
// The slots one side of the trade window has
static constexpr uint32_t MaxOfferedItems = 8;

View File

@@ -11,6 +11,8 @@ namespace {
{ 200, { 3040, 1 } },
{ 300, { 1727, 2, true, false } },
{ 400, { 1727, 2, false, true } },
{ 500, { 1727, 2, false, false, false, true } },
{ 600, { 6662, 1, false, false, true, false } },
};
class TradeAcceptTest : public GameDependenciesTest {
@@ -80,6 +82,43 @@ TEST(TradeOfferTest, AnEquippedItemIsRefused) {
EXPECT_FALSE(Trade::ResolveOffer({ { 400, 1727, 1 } }, FindHeld).has_value());
}
TEST(TradeOfferTest, AnItemThatMayNotBeDeletedIsRefused) {
EXPECT_FALSE(Trade::ResolveOffer({ { 500, 1727, 1 } }, FindHeld).has_value());
EXPECT_FALSE(Trade::ResolveOffer({ { 100, 1727, 1 }, { 500, 1727, 1 } }, FindHeld).has_value());
}
TEST(TradeOfferTest, AModelThatIsNotApprovedIsRefused) {
EXPECT_FALSE(Trade::ResolveOffer({ { 600, 6662, 1 } }, FindHeld).has_value());
EXPECT_FALSE(Trade::ResolveOffer({ { 100, 1727, 1 }, { 600, 6662, 1 } }, FindHeld).has_value());
}
TEST(TradeOfferTest, OnlyAnApprovedBrickBuiltModelIsApproved) {
LwoNameValue config;
// No moderation status
EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config));
config.Insert<int32_t>(u"userModelMod", 0);
EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config));
config.Insert<int32_t>(u"userModelMod", 2);
EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config));
// Not a number
config.Insert<bool>(u"userModelMod", true);
EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config));
config.Insert<int32_t>(u"userModelMod", Trade::ApprovedModerationStatus);
EXPECT_TRUE(Trade::IsApproved(Trade::BrickBuiltModelLot, config));
}
TEST(TradeOfferTest, AnItemThatIsNotAModelHasNoModerationStatus) {
LwoNameValue config;
EXPECT_TRUE(Trade::IsApproved(1727, config));
config.Insert<int32_t>(u"userModelMod", 0);
EXPECT_TRUE(Trade::IsApproved(1727, config));
}
TEST(TradeOfferTest, AnOfferFillsTheSlotsOfTheTradeWindowAtMost) {
const auto findAny = [](const LWOOBJID) -> std::optional<HeldTradeItem> {
return HeldTradeItem{ 1727, 1 };