diff --git a/dGame/TradingManager.cpp b/dGame/TradingManager.cpp index a125cb453..79032234e 100644 --- a/dGame/TradingManager.cpp +++ b/dGame/TradingManager.cpp @@ -43,7 +43,7 @@ std::optional> 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(LWOOBJID)> Trade::HeldIn(const Invent return [&inventory](const LWOOBJID itemId) -> std::optional { 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*>(status->second.get())->GetValue() == ApprovedModerationStatus; +} + Trade::~Trade() { } diff --git a/dGame/TradingManager.h b/dGame/TradingManager.h index d96a02a34..9fbc14801 100644 --- a/dGame/TradingManager.h +++ b/dGame/TradingManager.h @@ -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> ResolveOffer( const std::vector& offered, @@ -46,6 +52,18 @@ public: */ static std::function(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; diff --git a/tests/dGameTests/dGameMessagesTests/TradeOfferTests.cpp b/tests/dGameTests/dGameMessagesTests/TradeOfferTests.cpp index 17c9de852..464049e92 100644 --- a/tests/dGameTests/dGameMessagesTests/TradeOfferTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/TradeOfferTests.cpp @@ -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(u"userModelMod", 0); + EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config)); + + config.Insert(u"userModelMod", 2); + EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config)); + + // Not a number + config.Insert(u"userModelMod", true); + EXPECT_FALSE(Trade::IsApproved(Trade::BrickBuiltModelLot, config)); + + config.Insert(u"userModelMod", Trade::ApprovedModerationStatus); + EXPECT_TRUE(Trade::IsApproved(Trade::BrickBuiltModelLot, config)); +} + +TEST(TradeOfferTest, AnItemThatIsNotAModelHasNoModerationStatus) { + LwoNameValue config; + EXPECT_TRUE(Trade::IsApproved(1727, config)); + + config.Insert(u"userModelMod", 0); + EXPECT_TRUE(Trade::IsApproved(1727, config)); +} + TEST(TradeOfferTest, AnOfferFillsTheSlotsOfTheTradeWindowAtMost) { const auto findAny = [](const LWOOBJID) -> std::optional { return HeldTradeItem{ 1727, 1 };