#include "TradingManager.h" #include "BrickByBrick.h" #include "EconomyLedger.h" #include "dServer.h" #include "EntityManager.h" #include "GameMessages.h" #include "TradeMessages.h" #include "InventoryComponent.h" #include "ObjectIDManager.h" #include "Game.h" #include "Logger.h" #include "Item.h" #include "Character.h" #include "CharacterComponent.h" #include "MissionComponent.h" #include "eMissionTaskType.h" #include #include #include namespace { std::unique_ptr g_EmptyTrade; } TradingManager* TradingManager::m_Address = nullptr; Trade::Trade(LWOOBJID tradeId, LWOOBJID participantA, LWOOBJID participantB) { m_TradeId = tradeId; m_ParticipantA = participantA; m_ParticipantB = participantB; } std::optional> Trade::ResolveOffer( const std::vector& offered, const std::function(LWOOBJID)>& findHeld) { std::vector resolved; resolved.reserve(offered.size()); std::set seen; if (offered.size() > MaxOfferedItems) return std::nullopt; for (const auto& tradeItem : offered) { // The client keeps its offer by item ID, so an item is only ever in it once 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 || !held->deletable || !held->approved) return std::nullopt; resolved.push_back({ tradeItem.itemId, held->lot, std::min(tradeItem.itemCount, held->count) }); } return resolved; } std::function(LWOOBJID)> Trade::HeldIn(const InventoryComponent& inventory) { return [&inventory](const LWOOBJID itemId) -> std::optional { const auto* const item = inventory.FindItemById(itemId); if (!item) return std::nullopt; // The blueprint data sent for a stored model says it is approved (BlueprintMetadata), and the client goes // by that over the status the item itself carries const bool hasBlueprint = item->GetLot() == BrickBuiltModelLot && BrickByBrick::ConfigObjectId(item->GetConfig(), u"blueprintid") != LWOOBJID_EMPTY; return HeldTradeItem{ item->GetLot(), item->GetCount(), item->GetBound(), item->IsEquipped(), inventory.CanDelete(*item), hasBlueprint || 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() { } LWOOBJID Trade::GetTradeId() const { return m_TradeId; } bool Trade::IsParticipant(LWOOBJID playerId) const { return m_ParticipantA == playerId || m_ParticipantB == playerId; } LWOOBJID Trade::GetParticipantA() const { return m_ParticipantA; } LWOOBJID Trade::GetParticipantB() const { return m_ParticipantB; } Entity* Trade::GetParticipantAEntity() const { return Game::entityManager->GetEntity(m_ParticipantA); } Entity* Trade::GetParticipantBEntity() const { return Game::entityManager->GetEntity(m_ParticipantB); } void Trade::SetCoins(LWOOBJID participant, uint64_t coins) { if (participant == m_ParticipantA) { m_CoinsA = coins; } else if (participant == m_ParticipantB) { m_CoinsB = coins; } } void Trade::SetItems(LWOOBJID participant, std::vector items) { if (participant == m_ParticipantA) { m_ItemsA = items; } else if (participant == m_ParticipantB) { m_ItemsB = items; } } void Trade::SetAccepted(LWOOBJID participant, bool value) { if (participant == m_ParticipantA) { m_AcceptedA = !value; LOG("Accepted from A (%d), B: (%d)", value, m_AcceptedB); auto* entityB = GetParticipantBEntity(); if (entityB != nullptr) { GameMessages::ServerTradeAccept accept; accept.target = m_ParticipantB; accept.bFirst = value; accept.Send(entityB->GetSystemAddress()); } } else if (participant == m_ParticipantB) { m_AcceptedB = !value; LOG("Accepted from B (%d), A: (%d)", value, m_AcceptedA); auto* entityA = GetParticipantAEntity(); if (entityA != nullptr) { GameMessages::ServerTradeAccept accept; accept.target = m_ParticipantA; accept.bFirst = value; accept.Send(entityA->GetSystemAddress()); } } if (m_AcceptedA && m_AcceptedB) { auto* entityB = GetParticipantBEntity(); if (entityB != nullptr) { GameMessages::ServerTradeAccept accept; accept.target = m_ParticipantB; accept.bFirst = false; accept.Send(entityB->GetSystemAddress()); } else { return; } auto* entityA = GetParticipantAEntity(); if (entityA != nullptr) { GameMessages::ServerTradeAccept accept; accept.target = m_ParticipantA; accept.bFirst = false; accept.Send(entityA->GetSystemAddress()); } else { return; } Complete(); TradingManager::Instance()->CancelTrade(LWOOBJID_EMPTY, m_TradeId, false); } } bool Trade::GetAccepted(const LWOOBJID participant) const { if (participant == m_ParticipantA) return m_AcceptedA; if (participant == m_ParticipantB) return m_AcceptedB; return false; } void Trade::ResetAccepted() { m_AcceptedA = false; m_AcceptedB = false; } void Trade::Complete() { auto* entityA = GetParticipantAEntity(); auto* entityB = GetParticipantBEntity(); if (entityA == nullptr || entityB == nullptr) return; auto* inventoryA = entityA->GetComponent(); auto* inventoryB = entityB->GetComponent(); auto* missionsA = entityA->GetComponent(); auto* missionsB = entityB->GetComponent(); auto* characterA = entityA->GetCharacter(); auto* characterB = entityB->GetCharacter(); if (inventoryA == nullptr || inventoryB == nullptr || characterA == nullptr || characterB == nullptr || missionsA == nullptr || missionsB == nullptr) return; // First verify both players have the coins and items requested for the trade. if (characterA->GetCoins() < m_CoinsA || characterB->GetCoins() < m_CoinsB) { LOG("Possible coin trade cheating attempt! Aborting trade."); return; } // The inventories may have changed since the offers were made, so check the offers against them again if (ResolveOffer(m_ItemsA, HeldIn(*inventoryA)) != m_ItemsA || ResolveOffer(m_ItemsB, HeldIn(*inventoryB)) != m_ItemsB) { LOG("The items offered by %s and %s no longer match what they hold, aborting trade", characterA->GetName().c_str(), characterB->GetName().c_str()); return; } for (const auto& tradeItem : m_ItemsA) { auto* itemToRemove = inventoryA->FindItemById(tradeItem.itemId); if (itemToRemove) { if (itemToRemove->GetCount() < tradeItem.itemCount) { LOG("Possible cheating attempt from %s in trading!!! Aborting trade", characterA->GetName().c_str()); return; } } else { LOG("Possible cheating attempt from %s in trading due to item not being available!!!", characterA->GetName().c_str()); return; } } for (const auto& tradeItem : m_ItemsB) { auto* itemToRemove = inventoryB->FindItemById(tradeItem.itemId); if (itemToRemove) { if (itemToRemove->GetCount() < tradeItem.itemCount) { LOG("Possible cheating attempt from %s in trading!!! Aborting trade", characterB->GetName().c_str()); return; } } else { LOG("Possible cheating attempt from %s in trading due to item not being available!!! Aborting trade", characterB->GetName().c_str()); return; } } // Now actually do the trade. Nothing is created or destroyed, so the economy ledger records transfers instead. EconomyLedger::ScopedItemTransfer transfer; characterA->SetCoins(characterA->GetCoins() - m_CoinsA + m_CoinsB, eLootSourceType::TRADE, CoinSource::Trade(m_TradeId)); characterB->SetCoins(characterB->GetCoins() - m_CoinsB + m_CoinsA, eLootSourceType::TRADE, CoinSource::Trade(m_TradeId)); const auto zone = Game::server ? Game::server->GetZoneID() : 0; const auto recordCoins = [zone](const uint64_t coins, const LWOOBJID from, const LWOOBJID to) { if (coins == 0) return; EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::TRADE, .coins = static_cast(coins), .fromCharacter = from, .toCharacter = to, .zone = zone }); }; recordCoins(m_CoinsA, characterA->GetID(), characterB->GetID()); recordCoins(m_CoinsB, characterB->GetID(), characterA->GetID()); const auto tradeId = m_TradeId; const auto giveItems = [zone, tradeId](const std::vector& items, InventoryComponent* from, MissionComponent* fromMissions, InventoryComponent* to, const LWOOBJID fromCharacter, const LWOOBJID toCharacter) { for (const auto& tradeItem : items) { auto* itemToRemove = from->FindItemById(tradeItem.itemId); if (!itemToRemove || itemToRemove->GetCount() < tradeItem.itemCount) continue; // A whole item keeps its data (subkey, config) but gets a new object id, as upstream: reusing the id would // leave two objects with one id if the server stopped between the two players' saves. A stack merges into // the other player's stack. new_item_id records where the items ended up, so the chain can be followed. const bool whole = itemToRemove->GetCount() == tradeItem.itemCount; const auto config = itemToRemove->GetConfig(); const auto subKey = itemToRemove->GetSubKey(); const auto bound = itemToRemove->GetBound(); // Taking the whole stack removes the item, so read its LOT first const LOT lot = itemToRemove->GetLot(); itemToRemove->SetCount(itemToRemove->GetCount() - tradeItem.itemCount); fromMissions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -static_cast(tradeItem.itemCount)); // Live sent the trade's ID with the traded items ReceiveItemOptions options{}; options.metrics.transactionId = tradeId; const auto received = whole ? to->ReceiveItem(LWOOBJID_EMPTY, lot, tradeItem.itemCount, eLootSourceType::TRADE, config, subKey, bound, options) : to->ReceiveItem(LWOOBJID_EMPTY, lot, tradeItem.itemCount, eLootSourceType::TRADE, {}, LWOOBJID_EMPTY, false, options); EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::TRADE, .itemId = tradeItem.itemId, .newItemId = received.id, .lot = lot, .count = tradeItem.itemCount, .fromCharacter = fromCharacter, .toCharacter = toCharacter, .zone = zone, .merged = received.merged }); } }; giveItems(m_ItemsA, inventoryA, missionsA, inventoryB, characterA->GetID(), characterB->GetID()); giveItems(m_ItemsB, inventoryB, missionsB, inventoryA, characterB->GetID(), characterA->GetID()); characterA->SaveXMLToDatabase(); characterB->SaveXMLToDatabase(); return; } void Trade::Cancel(const LWOOBJID canceller) { auto* entityA = GetParticipantAEntity(); auto* entityB = GetParticipantBEntity(); if (entityA == nullptr || entityB == nullptr) return; if (entityA->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) { GameMessages::ServerTradeCancel cancel; cancel.target = entityA->GetObjectID(); cancel.Send(entityA->GetSystemAddress()); } if (entityB->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) { GameMessages::ServerTradeCancel cancel; cancel.target = entityB->GetObjectID(); cancel.Send(entityB->GetSystemAddress()); } } void Trade::SendUpdateToOther(LWOOBJID participant) { Entity* other = nullptr; Entity* self = nullptr; uint64_t coins; std::vector itemIds; LOG("Attempting to send trade update"); if (participant == m_ParticipantA) { other = GetParticipantBEntity(); self = GetParticipantAEntity(); coins = m_CoinsA; itemIds = m_ItemsA; } else if (participant == m_ParticipantB) { other = GetParticipantAEntity(); self = GetParticipantBEntity(); coins = m_CoinsB; itemIds = m_ItemsB; } else { return; } if (other == nullptr || self == nullptr) return; std::vector items{}; auto* inventoryComponent = self->GetComponent(); if (inventoryComponent == nullptr) return; for (const auto tradeItem : itemIds) { auto* item = inventoryComponent->FindItemById(tradeItem.itemId); if (item == nullptr) return; if (tradeItem.itemCount > item->GetCount()) return; items.push_back(tradeItem); } LOG("Sending trade update"); GameMessages::ServerTradeUpdate update; update.target = other->GetObjectID(); update.i64Currency = coins; for (const auto& item : items) { GameMessages::TradeItemEntry entry; entry.key = item.itemId; entry.itemID = item.itemId; entry.templateID = item.itemLot; entry.count = item.itemCount; // the count is always written, even when it is 1 update.inventoryMap.push_back(entry); } update.Send(other->GetSystemAddress()); } const std::unique_ptr& TradingManager::GetTrade(LWOOBJID tradeId) const { const auto& pair = trades.find(tradeId); if (pair == trades.end()) return g_EmptyTrade; return pair->second; } const std::unique_ptr& TradingManager::GetPlayerTrade(LWOOBJID playerId) const { for (const auto& trade : trades | std::views::values) { if (trade->IsParticipant(playerId)) { return trade; } } return g_EmptyTrade; } void TradingManager::CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage) { const auto& trade = GetTrade(tradeId); if (trade == nullptr) return; if (sendCancelMessage) trade->Cancel(canceller); trades.erase(tradeId); } void TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) { const LWOOBJID tradeId = ObjectIDManager::GenerateObjectID(); trades.insert_or_assign(tradeId, std::make_unique(tradeId, participantA, participantB)); LOG("Created new trade between (%llu) <-> (%llu)", participantA, participantB); }