#include "InventoryComponent.h" #include "BrickByBrick.h" #include "Contraband.h" #include "EconomyLedger.h" #include #include "Entity.h" #include "Item.h" #include "Game.h" #include "dServer.h" #include "Logger.h" #include "CDClientManager.h" #include "CDDeletionRestrictionsTable.h" #include "eGameMasterLevel.h" #include #include "ObjectIDManager.h" #include "MissionComponent.h" #include "GameMessages.h" #include "InventoryMessages.h" #include "Database.h" #include "CDModularBuildComponentTable.h" #include "SkillMessages.h" #include "MovementMessages.h" #include "SkillComponent.h" #include "Character.h" #include "EntityManager.h" #include "ItemSet.h" #include "PetComponent.h" #include "PossessorComponent.h" #include "PossessableComponent.h" #include "ModuleAssemblyComponent.h" #include "HavokVehiclePhysicsComponent.h" #include "CharacterComponent.h" #include "dZoneManager.h" #include "PropertyManagementComponent.h" #include "DestroyableComponent.h" #include "dConfig.h" #include "eItemType.h" #include "eUnequippableActiveType.h" #include "CppScripts.h" #include "eMissionTaskType.h" #include "eStateChangeType.h" #include "eUseItemResponse.h" #include "Mail.h" #include "ProximityMonitorComponent.h" #include "CDComponentsRegistryTable.h" #include "CDInventoryComponentTable.h" #include "CDScriptComponentTable.h" #include "CDObjectSkillsTable.h" #include "CDSkillBehaviorTable.h" #include "StringifiedEnum.h" #include "Amf3.h" #include InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { RegisterMsg(&InventoryComponent::OnGetObjectReportInfo); this->m_Dirty = true; this->m_Equipped = {}; this->m_Pushed = {}; this->m_Consumable = LOT_NULL; this->m_Pets = {}; const auto lot = parent->GetLOT(); if (lot == 1) { auto* character = m_Parent->GetCharacter(); if (character) LoadXml(character->GetXMLDoc()); CheckProxyIntegrity(); return; } auto* compRegistryTable = CDClientManager::GetTable(); const auto componentId = compRegistryTable->GetByIDAndType(lot, eReplicaComponentType::INVENTORY); auto* inventoryComponentTable = CDClientManager::GetTable(); auto items = inventoryComponentTable->Query([=](const CDInventoryComponent entry) { return entry.id == componentId; }); auto slot = 0u; for (const auto& item : items) { if (!Inventory::IsValidItem(item.itemid)) continue; AddItem(item.itemid, item.count); if (!item.equip) continue; const LWOOBJID id = ObjectIDManager::GenerateObjectID(); const auto& info = Inventory::FindItemComponent(item.itemid); UpdateSlot(info.equipLocation, { id, static_cast(item.itemid), item.count, slot++ }); // Equip this items proxies. auto subItems = info.subItems; subItems.erase(std::remove_if(subItems.begin(), subItems.end(), ::isspace), subItems.end()); if (!subItems.empty()) { const auto subItemsSplit = GeneralUtils::SplitString(subItems, ','); for (auto proxyLotAsString : subItemsSplit) { const auto proxyLOT = static_cast(std::stoi(proxyLotAsString)); const auto& proxyInfo = Inventory::FindItemComponent(proxyLOT); const LWOOBJID proxyId = ObjectIDManager::GenerateObjectID(); // Use item.count since we equip item.count number of the item this is a requested proxy of UpdateSlot(proxyInfo.equipLocation, { proxyId, proxyLOT, item.count, slot++ }); } } } } Inventory* InventoryComponent::GetInventory(const eInventoryType type) { const auto index = m_Inventories.find(type); if (index != m_Inventories.end()) { return index->second; } // Create new empty inventory uint32_t size = 240u; switch (type) { case eInventoryType::ITEMS: size = 20u; break; case eInventoryType::VAULT_MODELS: case eInventoryType::VAULT_ITEMS: size = 40u; break; case eInventoryType::VENDOR_BUYBACK: size = 27u; break; case eInventoryType::DONATION: size = 24u; break; default: break; } auto* inventory = new Inventory(type, size, {}, this); m_Inventories.insert_or_assign(type, inventory); return inventory; } const std::map& InventoryComponent::GetInventories() const { return m_Inventories; } uint32_t InventoryComponent::GetLotCount(const LOT lot) const { uint32_t count = 0; for (const auto& inventory : m_Inventories) { count += inventory.second->GetLotCount(lot); } return count; } uint32_t InventoryComponent::GetLotCountNonTransfer(LOT lot, bool includeVault) const { uint32_t count = 0; for (const auto& inventory : m_Inventories) { if (IsTransferInventory(inventory.second->GetType(), includeVault)) continue; count += inventory.second->GetLotCount(lot); } return count; } const EquipmentMap& InventoryComponent::GetEquippedItems() const { return m_Equipped; } void InventoryComponent::AddItem( const LOT lot, const uint32_t count, eLootSourceType lootSourceType, eInventoryType inventoryType, const LwoNameValue& config, const LWOOBJID parent, const bool showFlyingLoot, bool isModMoveAndEquip, const LWOOBJID subKey, const eInventoryType inventorySourceType, const int32_t sourceType, const bool bound, int32_t preferredSlot) { if (count == 0) { LOG("Attempted to add 0 of item (%i) to the inventory!", lot); return; } if (!Inventory::IsValidItem(lot)) { if (lot > 0) { LOG("Attempted to add invalid item (%i) to the inventory!", lot); } return; } if (inventoryType == INVALID) { inventoryType = Inventory::FindInventoryTypeForLot(lot); } Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, inventorySourceType); auto* missions = static_cast(this->m_Parent->GetComponent(eReplicaComponentType::MISSION)); auto* inventory = GetInventory(inventoryType); if (!config.values.empty() || bound) { const auto slot = preferredSlot != -1 && inventory->IsSlotEmpty(preferredSlot) ? preferredSlot : inventory->FindEmptySlot(); if (slot == -1) { LOG("Failed to find empty slot for inventory (%i)!", inventoryType); return; } auto* item = new Item(lot, inventory, slot, count, config, parent, showFlyingLoot, isModMoveAndEquip, subKey, bound, lootSourceType); if (missions != nullptr && !IsTransferInventory(inventoryType)) { missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count, IsTransferInventory(inventorySourceType)); } return; } const auto info = Inventory::FindItemComponent(lot); auto left = count; int32_t outOfSpace = 0; auto stack = static_cast(info.stackSize); bool isBrick = inventoryType == eInventoryType::BRICKS || (stack == 0 && info.itemType == 1); // info.itemType of 1 is item type brick if (isBrick) { stack = UINT32_MAX; } else if (stack == 0) { stack = 1; } auto* existing = FindItemByLot(lot, inventoryType); if (existing != nullptr) { const auto delta = std::min(left, stack - existing->GetCount()); left -= delta; existing->SetCount(existing->GetCount() + delta, false, true, showFlyingLoot, lootSourceType); if (isModMoveAndEquip) { existing->Equip(); isModMoveAndEquip = false; } } // If we have some leftover and we aren't bricks, make a new stack while (left > 0 && (!isBrick || (isBrick && !existing))) { const auto size = std::min(left, stack); left -= size; int32_t slot; if (preferredSlot != -1 && inventory->IsSlotEmpty(preferredSlot)) { slot = preferredSlot; preferredSlot = -1; } else { slot = inventory->FindEmptySlot(); } if (slot == -1) { outOfSpace += size; switch (sourceType) { case 0: Mail::SendMail(m_Parent, "%[MAIL_ACTIVITY_OVERFLOW_HEADER]", "%[MAIL_ACTIVITY_OVERFLOW_BODY]", lot, size); break; case 1: for (size_t i = 0; i < size; i++) { GameMessages::DropClientLoot lootMsg{}; lootMsg.target = m_Parent->GetObjectID(); lootMsg.ownerID = m_Parent->GetObjectID(); lootMsg.sourceID = m_Parent->GetObjectID(); lootMsg.item = lot; lootMsg.count = 1; lootMsg.spawnPos = m_Parent->GetPosition(); Loot::DropItem(*m_Parent, lootMsg); } break; default: break; } continue; } auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType); isModMoveAndEquip = false; } if (missions != nullptr && !IsTransferInventory(inventoryType)) { missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count - outOfSpace, IsTransferInventory(inventorySourceType)); } } ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, const uint32_t count, eLootSourceType lootSourceType, const LwoNameValue& config, const LWOOBJID subKey, const bool bound, const ReceiveItemOptions& options) { if (count == 0 || !Inventory::IsValidItem(lot)) return {}; const auto inventoryType = options.inventory != INVALID ? options.inventory : Inventory::FindInventoryTypeForLot(lot); auto* inventory = GetInventory(inventoryType); if (!inventory) return {}; const bool unique = !config.values.empty() || subKey != LWOOBJID_EMPTY || bound || IsUniqueLot(lot); const bool keepId = id != LWOOBJID_EMPTY && FindItemById(id) == nullptr && (unique || inventory->FindItemByLot(lot) == nullptr); int32_t slot = -1; if (keepId) slot = options.preferredSlot != -1 && inventory->IsSlotEmpty(options.preferredSlot) ? options.preferredSlot : inventory->FindEmptySlot(); if (slot != -1) { auto* item = new Item(id, lot, inventory, slot, count, bound || Inventory::FindItemComponent(lot).isBOP, config, LWOOBJID_EMPTY, subKey, lootSourceType); GameMessages::AddItemToInventoryClientSync addItem; addItem.target = m_Parent->GetObjectID(); addItem.SetItem(*item); addItem.eLootTypeSource = lootSourceType; addItem.iSubkey = subKey; addItem.itemCount = static_cast(count); addItem.newObjID = id; addItem.showFlyingLoot = options.showFlyingLoot && !options.equip; addItem.SendToClient(m_Parent->GetSystemAddress()); EconomyLedger::RecordItems(m_Parent, lot, count, static_cast(lootSourceType)); Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, options.sourceInventory); if (options.equip) { item->Equip(); Game::entityManager->SerializeEntity(m_Parent); } auto* missions = m_Parent->GetComponent(); if (missions && !IsTransferInventory(inventoryType)) missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count, IsTransferInventory(options.sourceInventory)); return { id, false }; } // Merge (or add a new stack) the normal way, then see which stack took the items std::map before; for (const auto& [itemId, item] : inventory->GetItems()) { if (item->GetLot() == lot) before[itemId] = item->GetCount(); } AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot); LWOOBJID receivedBy = LWOOBJID_EMPTY; int64_t largestGain = 0; for (const auto& [itemId, item] : inventory->GetItems()) { if (item->GetLot() != lot) continue; const auto previous = before.find(itemId); const int64_t gain = static_cast(item->GetCount()) - (previous == before.end() ? 0 : previous->second); if (gain > largestGain) { largestGain = gain; receivedBy = itemId; } } return { receivedBy, before.contains(receivedBy) }; } bool InventoryComponent::IsUniqueLot(const LOT lot) { const auto& info = Inventory::FindItemComponent(lot); const bool isBrick = Inventory::FindInventoryTypeForLot(lot) == eInventoryType::BRICKS || (info.stackSize == 0 && info.itemType == 1); return !isBrick && info.stackSize <= 1; } bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent) { if (count == 0) { LOG("Attempted to remove 0 of item (%i) from the inventory!", lot); return false; } if (inventoryType != eInventoryType::ALL) { if (inventoryType == INVALID) inventoryType = Inventory::FindInventoryTypeForLot(lot); auto* inventory = GetInventory(inventoryType); if (!inventory) return false; auto left = std::min(count, inventory->GetLotCount(lot)); if (left != count) return false; while (left > 0) { auto* item = FindItemByLot(lot, inventoryType, false, ignoreBound); if (!item) break; const auto delta = std::min(left, item->GetCount()); item->SetCount(item->GetCount() - delta, silent); left -= delta; } return true; } else { auto left = count; for (const auto& inventory : m_Inventories | std::views::values) { while (left > 0 && inventory->GetLotCount(lot) > 0) { auto* item = inventory->FindItemByLot(lot, false, ignoreBound); if (!item) break; const auto delta = std::min(item->GetCount(), left); item->SetCount(item->GetCount() - delta, silent); left -= delta; } } return left == 0; } } void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType inventory, const uint32_t count, const bool showFlyingLot, bool isModMoveAndEquip, const bool ignoreEquipped, const int32_t preferredSlot) { if (item == nullptr) { return; } auto* origin = item->GetInventory(); const auto lot = item->GetLot(); const auto subkey = item->GetSubKey(); // Moving between a player's own inventories neither creates nor destroys anything, except for the vendor's // buyback inventory: selling takes the item out of the game until it is bought back EconomyLedger::ScopedItemTransfer moving; const auto originType = origin->GetType(); uint32_t moved = 0; // Each move gives the items a new object id in the other inventory (as upstream, and as live did), so the ledger // records it for item traces: the old id, the stack that took the items and whether that stack was already there auto* character = m_Parent->GetCharacter(); const auto zone = Game::server ? Game::server->GetZoneID() : 0; const auto recordMove = [&](const LWOOBJID oldId, const ReceivedItem& received, const uint32_t moveCount) { if (!character || received.id == LWOOBJID_EMPTY || received.id == oldId) return; EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::INVENTORY_MOVE, .itemId = oldId, .newItemId = received.id, .lot = lot, .count = moveCount, .fromCharacter = character->GetID(), .toCharacter = character->GetID(), .zone = zone, .merged = received.merged }); }; const ReceiveItemOptions moveOptions{ .inventory = inventory, .showFlyingLoot = showFlyingLot, .preferredSlot = preferredSlot, .sourceInventory = originType }; // Items move the way upstream moves them (a new object in the other inventory); only the ledger bookkeeping is added if (subkey == LWOOBJID_EMPTY && item->GetConfig().values.empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) { auto left = std::min(count, origin->GetLotCount(lot)); moved = left; while (left > 0) { if (item == nullptr) { item = origin->FindItemByLot(lot, false); if (item == nullptr) { break; } } const auto delta = std::min(item->GetCount(), left); left -= delta; auto options = moveOptions; options.equip = isModMoveAndEquip; const auto oldId = item->GetId(); const auto received = ReceiveItem(LWOOBJID_EMPTY, lot, delta, eLootSourceType::NONE, {}, LWOOBJID_EMPTY, false, options); item->SetCount(item->GetCount() - delta, false, false); recordMove(oldId, received, delta); isModMoveAndEquip = false; } } else { const auto config = item->GetConfig(); const auto delta = std::min(item->GetCount(), count); moved = delta; auto options = moveOptions; options.equip = isModMoveAndEquip; const auto oldId = item->GetId(); const auto received = ReceiveItem(LWOOBJID_EMPTY, lot, delta, eLootSourceType::NONE, config, subkey, item->GetBound(), options); item->SetCount(item->GetCount() - delta, false, false); recordMove(oldId, received, delta); } if (inventory == eInventoryType::VENDOR_BUYBACK && originType != eInventoryType::VENDOR_BUYBACK) { EconomyLedger::RecordItemsUnsuppressed(m_Parent, lot, -static_cast(moved), static_cast(eLootSourceType::VENDOR)); } else if (originType == eInventoryType::VENDOR_BUYBACK && inventory != eInventoryType::VENDOR_BUYBACK) { EconomyLedger::RecordItemsUnsuppressed(m_Parent, lot, moved, static_cast(eLootSourceType::VENDOR)); } auto* missionComponent = m_Parent->GetComponent(); if (missionComponent != nullptr) { if (IsTransferInventory(inventory)) { missionComponent->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -static_cast(count)); } } } void InventoryComponent::MoveStack(Item* item, const eInventoryType inventory, const uint32_t slot) { if (inventory != INVALID && item->GetInventory()->GetType() != inventory) { auto* newInventory = GetInventory(inventory); item->SetInventory(newInventory); } item->SetSlot(slot); } Item* InventoryComponent::FindItemById(const LWOOBJID id) const { for (const auto& inventory : m_Inventories) { auto* item = inventory.second->FindItemById(id); if (item != nullptr) { return item; } } return nullptr; } Item* InventoryComponent::FindItemByLot(const LOT lot, eInventoryType inventoryType, const bool ignoreEquipped, const bool ignoreBound) { if (inventoryType == INVALID) { inventoryType = Inventory::FindInventoryTypeForLot(lot); } auto* inventory = GetInventory(inventoryType); return inventory->FindItemByLot(lot, ignoreEquipped, ignoreBound); } Item* InventoryComponent::FindItemBySubKey(LWOOBJID id, eInventoryType inventoryType) { if (inventoryType == INVALID) { for (const auto& inventory : m_Inventories) { auto* item = inventory.second->FindItemBySubKey(id); if (item != nullptr) { return item; } } return nullptr; } else { return GetInventory(inventoryType)->FindItemBySubKey(id); } } bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) { std::unordered_map spaceOffset{}; uint32_t slotsNeeded = 0; for (const auto& pair : loot) { const auto inventoryType = Inventory::FindInventoryTypeForLot(pair.first); if (inventoryType == BRICKS) { continue; } auto* inventory = GetInventory(inventoryType); if (inventory == nullptr) { return false; } const auto info = Inventory::FindItemComponent(pair.first); auto stack = static_cast(info.stackSize); auto left = pair.second; auto* partial = inventory->FindItemByLot(pair.first); if (partial != nullptr && partial->GetCount() < stack) { left -= stack - partial->GetCount(); } auto requiredSlots = std::ceil(static_cast(left) / stack); const auto& offsetIter = spaceOffset.find(inventoryType); auto freeSpace = inventory->GetEmptySlots() - (offsetIter == spaceOffset.end() ? 0 : offsetIter->second); if (requiredSlots > freeSpace) { slotsNeeded += requiredSlots - freeSpace; } spaceOffset[inventoryType] = offsetIter == spaceOffset.end() ? requiredSlots : offsetIter->second + requiredSlots; } if (slotsNeeded > 0) { GameMessages::NotifyNotEnoughInvSpace notEnoughSpace; notEnoughSpace.target = m_Parent->GetObjectID(); notEnoughSpace.freeSlotsNeeded = slotsNeeded; notEnoughSpace.inventoryType = ITEMS; notEnoughSpace.Send(m_Parent->GetSystemAddress()); return false; } return true; } namespace { /** * Cars and rockets built before builds were recorded have no subkey, so the client has no blueprint id to ask for * their icon with. Give such an item what a new build gets (ModularBuildFinish): a persistent id as its subkey and a * ugc_modular_build row with its modules. The next save keeps the subkey. Main thread (character load). */ void AssignModularBuildId(Item& item, Entity* owner) { if (item.GetSubKey() != LWOOBJID_EMPTY || !CDClientManager::GetTable()->IsCreatedLot(item.GetLot())) return; const auto& config = item.GetConfig(); const auto modules = config.find(u"assemblyPartLOTs"); if (modules == config.values.end() || !modules->second) return; const auto modulesText = modules->second->GetValueAsString(); if (modulesText.empty()) return; const auto id = ObjectIDManager::GetPersistentID(); auto* character = owner ? owner->GetCharacter() : nullptr; Database::Get()->InsertUgcBuild(modulesText, id, character ? std::optional(character->GetID()) : std::nullopt); item.SetSubKey(id); LOG("Gave the %i build %llu (made before builds were recorded) the build id %llu", item.GetLot(), item.GetId(), id); } } void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) { LoadPetXml(document); auto* inventoryElement = document.FirstChildElement("obj")->FirstChildElement("inv"); if (inventoryElement == nullptr) { LOG("Failed to find 'inv' xml element!"); return; } auto* bags = inventoryElement->FirstChildElement("bag"); if (bags == nullptr) { LOG("Failed to find 'bags' xml element!"); return; } auto* const groups = inventoryElement->FirstChildElement("grps"); if (groups) { LoadGroupXml(*groups); } m_Consumable = inventoryElement->IntAttribute("csl", LOT_NULL); auto* bag = bags->FirstChildElement(); while (bag != nullptr) { unsigned int type; unsigned int size; bag->QueryAttribute("t", &type); bag->QueryAttribute("m", &size); auto* inventory = GetInventory(static_cast(type)); inventory->SetSize(size); bag = bag->NextSiblingElement(); } auto* items = inventoryElement->FirstChildElement("items"); if (items == nullptr) { LOG("Failed to find 'items' xml element!"); return; } bag = items->FirstChildElement(); while (bag != nullptr) { unsigned int type; bag->QueryAttribute("t", &type); // A build in progress does not survive a reload: what was in the BBB inventories is back in MODELS and BRICKS const auto savedType = type; type = BrickByBrick::InventoryToLoadInto(type); auto* inventory = GetInventory(static_cast(type)); if (inventory == nullptr) { LOG("Failed to find inventory (%i)!", type); return; } auto* itemElement = bag->FirstChildElement(); while (itemElement != nullptr) { LWOOBJID id; LOT lot; bool equipped; unsigned int slot; unsigned int count; bool bound; LWOOBJID subKey = LWOOBJID_EMPTY; itemElement->QueryAttribute("id", &id); itemElement->QueryAttribute("l", &lot); itemElement->QueryAttribute("eq", &equipped); itemElement->QueryAttribute("s", &slot); itemElement->QueryAttribute("c", &count); itemElement->QueryAttribute("b", &bound); itemElement->QueryAttribute("sk", &subKey); // Begin custom xml auto parent = LWOOBJID_EMPTY; itemElement->QueryAttribute("parent", &parent); // End custom xml if (savedType != type && !inventory->IsSlotEmpty(slot)) { const auto freeSlot = inventory->FindEmptySlot(); if (freeSlot == -1) inventory->SetSize(inventory->GetSize() + 1); slot = freeSlot != -1 ? freeSlot : inventory->FindEmptySlot(); } auto* item = new Item(id, lot, inventory, slot, count, bound, {}, parent, subKey); item->LoadConfigXml(*itemElement); AssignModularBuildId(*item, m_Parent); if (equipped) { const auto info = Inventory::FindItemComponent(lot); UpdateSlot(info.equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot() }); AddItemSkills(item->GetLot()); } itemElement = itemElement->NextSiblingElement(); } bag = bag->NextSiblingElement(); } for (const auto inventory : m_Inventories) { const auto itemCount = inventory.second->GetItems().size(); if (inventory.second->GetSize() < itemCount) { inventory.second->SetSize(itemCount); } } } void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) { UpdatePetXml(document); auto* inventoryElement = document.FirstChildElement("obj")->FirstChildElement("inv"); if (inventoryElement == nullptr) { LOG("Failed to find 'inv' xml element!"); return; } std::vector inventoriesToSave; // Need to prevent some transfer inventories from being saved for (const auto& pair : this->m_Inventories) { auto* inventory = pair.second; // MODELS_IN_BBB is saved so a model open in brick by brick building survives a disconnect or a crash; it loads // back into MODELS (BrickByBrick::InventoryToLoadInto) static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, ITEM_SETS }; if (std::ranges::find(EXCLUDED_INVENTORIES, inventory->GetType()) != EXCLUDED_INVENTORIES.end()) { continue; } inventoriesToSave.push_back(inventory); } inventoryElement->SetAttribute("csl", m_Consumable); auto* bags = inventoryElement->FirstChildElement("bag"); if (bags == nullptr) { LOG("Failed to find 'bags' xml element!"); return; } bags->DeleteChildren(); for (const auto* inventory : inventoriesToSave) { auto* bag = document.NewElement("b"); bag->SetAttribute("t", inventory->GetType()); bag->SetAttribute("m", static_cast(inventory->GetSize())); bags->LinkEndChild(bag); } auto* groups = inventoryElement->FirstChildElement("grps"); if (groups) { groups->DeleteChildren(); } else { groups = inventoryElement->InsertNewChildElement("grps"); } UpdateGroupXml(*groups); auto* items = inventoryElement->FirstChildElement("items"); if (items == nullptr) { LOG("Failed to find 'items' xml element!"); return; } items->DeleteChildren(); for (auto* inventory : inventoriesToSave) { if (inventory->GetSize() == 0) { continue; } auto* bagElement = document.NewElement("in"); bagElement->SetAttribute("t", inventory->GetType()); for (const auto& pair : inventory->GetItems()) { auto* item = pair.second; auto* itemElement = document.NewElement("i"); itemElement->SetAttribute("l", item->GetLot()); itemElement->SetAttribute("id", item->GetId()); itemElement->SetAttribute("s", static_cast(item->GetSlot())); itemElement->SetAttribute("c", static_cast(item->GetCount())); itemElement->SetAttribute("b", item->GetBound()); itemElement->SetAttribute("eq", item->IsEquipped()); itemElement->SetAttribute("sk", item->GetSubKey()); // Begin custom xml itemElement->SetAttribute("parent", item->GetParent()); // End custom xml item->SaveConfigXml(*itemElement); bagElement->LinkEndChild(itemElement); } items->LinkEndChild(bagElement); } } void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool bIsInitialUpdate) { if (bIsInitialUpdate || m_Dirty) { outBitStream.Write(true); outBitStream.Write(m_Equipped.size()); for (const auto& pair : m_Equipped) { const auto item = pair.second; outBitStream.Write(item.id); outBitStream.Write(item.lot); outBitStream.Write0(); outBitStream.Write(item.count > 0); if (item.count > 0) outBitStream.Write(item.count); outBitStream.Write(item.slot != 0); if (item.slot != 0) outBitStream.Write(item.slot); outBitStream.Write0(); bool flag = !item.config.values.empty(); outBitStream.Write(flag); if (flag) { RakNet::BitStream ldfStream; ldfStream.Write(item.config.values.size()); // Key count for (const auto& data : item.config.values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { std::string newRocketStr = data->GetValueAsString() + ";"; GeneralUtils::ReplaceInString(newRocketStr, "+", ";"); LDFData ldf_data(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr)); ldf_data.WriteToPacket(ldfStream); } else { data->WriteToPacket(ldfStream); } } outBitStream.Write(ldfStream.GetNumberOfBytesUsed() + 1); outBitStream.Write(0); // Don't compress outBitStream.Write(ldfStream); } outBitStream.Write1(); } m_Dirty = false; } else { outBitStream.Write(false); } outBitStream.Write(false); } void InventoryComponent::Update(float deltaTime) { for (auto* set : m_Itemsets) { set->Update(deltaTime); } } void InventoryComponent::UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent) { const auto index = m_Equipped.find(location); if (index != m_Equipped.end()) { if (keepCurrent) { m_Equipped.insert_or_assign(location + std::to_string(m_Equipped.size()), item); m_Dirty = true; return; } auto* old = FindItemById(index->second.id); if (old != nullptr) { UnEquipItem(old); } } m_Equipped.insert_or_assign(location, item); m_Dirty = true; } void InventoryComponent::RemoveSlot(const std::string& location) { if (m_Equipped.find(location) == m_Equipped.end()) { return; } m_Equipped.erase(location); m_Dirty = true; } void InventoryComponent::EquipItem(Item* item, const bool skipChecks) { if (!Inventory::IsValidItem(item->GetLot())) return; // Temp items should be equippable but other transfer items shouldn't be (for example the instruments in RB) if (item->IsEquipped() || (item->GetInventory()->GetType() != TEMP_ITEMS && IsTransferInventory(item->GetInventory()->GetType())) || IsPet(item->GetSubKey())) { return; } auto* character = m_Parent->GetCharacter(); if (character != nullptr && !skipChecks) { // Hacky proximity rocket if (item->GetLot() == 6416) { const auto rocketLauchPads = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::ROCKET_LAUNCH); const auto position = m_Parent->GetPosition(); for (auto* launchPad : rocketLauchPads) { if (!launchPad) continue; auto prereq = launchPad->GetVarAsString(u"rocketLaunchPreCondition"); if (!prereq.empty()) { PreconditionExpression expression(prereq); if (!expression.Check(m_Parent)) continue; } if (Vector3::DistanceSquared(launchPad->GetPosition(), position) > 13 * 13) continue; auto* characterComponent = m_Parent->GetComponent(); if (characterComponent != nullptr) characterComponent->SetLastRocketItemID(item->GetId()); launchPad->OnUse(m_Parent); break; } return; } else if (item->GetLot() == 8092) { // Trying to equip a car const auto proximityObjects = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROXIMITY_MONITOR); // look for car instancers and check if we are in its setup range for (auto* const entity : proximityObjects) { if (!entity) continue; auto* proximityMonitorComponent = entity->GetComponent(); if (!proximityMonitorComponent) continue; if (proximityMonitorComponent->IsInProximity("Interaction_Distance", m_Parent->GetObjectID())) { // in the range of a car instancer entity->OnUse(m_Parent); GameMessages::UseItemOnClient itemMsg; itemMsg.target = entity->GetObjectID(); itemMsg.itemLOT = item->GetLot(); itemMsg.itemToUse = item->GetId(); itemMsg.playerId = m_Parent->GetObjectID(); itemMsg.Send(m_Parent->GetSystemAddress()); break; } } return; } const auto building = character->GetBuildMode(); const auto type = static_cast(item->GetInfo().itemType); if (!building && (item->GetLot() == 6086 || type == eItemType::LOOT_MODEL || type == eItemType::VEHICLE)) return; if (type != eItemType::LOOT_MODEL && type != eItemType::MODEL) { if (!item->GetBound() && !item->GetPreconditionExpression()->Check(m_Parent)) { return; } } } const auto lot = item->GetLot(); CheckItemSet(lot); for (auto* set : m_Itemsets) { set->OnEquip(lot); } if (item->GetInfo().isBOE) item->SetBound(true); GenerateProxies(item); UpdateSlot(item->GetInfo().equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot(), item->GetConfig() }); ApplyBuff(item); AddItemSkills(item->GetLot()); EquipScripts(item); Game::entityManager->SerializeEntity(m_Parent); } void InventoryComponent::UnEquipItem(Item* item) { if (!item->IsEquipped()) { return; } const auto lot = item->GetLot(); if (!Inventory::IsValidItem(lot)) { return; } CheckItemSet(lot); for (auto* set : m_Itemsets) { set->OnUnEquip(lot); } RemoveBuff(item); RemoveItemSkills(item->GetLot()); RemoveSlot(item->GetInfo().equipLocation); UnequipScripts(item); Game::entityManager->SerializeEntity(m_Parent); // Trigger property event if (PropertyManagementComponent::Instance() != nullptr && item->GetCount() > 0 && Inventory::FindInventoryTypeForLot(item->GetLot()) == MODELS) { PropertyManagementComponent::Instance()->GetParent()->OnZonePropertyModelRemovedWhileEquipped(m_Parent); Game::zoneManager->GetZoneControlObject()->OnZonePropertyModelRemovedWhileEquipped(m_Parent); } PurgeProxies(item); } void InventoryComponent::EquipScripts(Item* equippedItem) { CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); if (!compRegistryTable) return; int32_t scriptComponentID = compRegistryTable->GetByIDAndType(equippedItem->GetLot(), eReplicaComponentType::SCRIPT, -1); if (scriptComponentID > -1) { CDScriptComponentTable* scriptCompTable = CDClientManager::GetTable(); CDScriptComponent scriptCompData = scriptCompTable->GetByID(scriptComponentID); auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); } else { itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId()); } } } void InventoryComponent::UnequipScripts(Item* unequippedItem) { CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); if (!compRegistryTable) return; int32_t scriptComponentID = compRegistryTable->GetByIDAndType(unequippedItem->GetLot(), eReplicaComponentType::SCRIPT, -1); if (scriptComponentID > -1) { CDScriptComponentTable* scriptCompTable = CDClientManager::GetTable(); CDScriptComponent scriptCompData = scriptCompTable->GetByID(scriptComponentID); auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); } else { itemScript->OnFactionTriggerItemUnequipped(m_Parent, unequippedItem->GetId()); } } } void InventoryComponent::HandlePossession(Item* item) { auto* possessorComponent = m_Parent->GetComponent(); if (!possessorComponent) return; // Don't do anything if we are busy dismounting if (possessorComponent->GetIsDismounting()) return; // Check to see if we are already mounting something auto* currentlyPossessedEntity = Game::entityManager->GetEntity(possessorComponent->GetPossessable()); auto currentlyPossessedItem = possessorComponent->GetMountItemID(); if (currentlyPossessedItem) { if (currentlyPossessedEntity) possessorComponent->Dismount(currentlyPossessedEntity); return; } // Set the mount item ID so that we know what we're handling possessorComponent->SetMountItemID(item->GetId()); GameMessages::SetMountInventoryID mountInventoryID; mountInventoryID.target = m_Parent->GetObjectID(); mountInventoryID.inventoryMountID = item->GetId(); mountInventoryID.Send(UNASSIGNED_SYSTEM_ADDRESS); // Create the mount entity EntityInfo info{}; info.lot = item->GetLot(); info.pos = m_Parent->GetPosition(); info.rot = m_Parent->GetRotation(); info.spawnerID = m_Parent->GetObjectID(); auto* mount = Game::entityManager->CreateEntity(info, nullptr, m_Parent); auto* possessableComponent = mount->GetComponent(); if (possessableComponent) { possessableComponent->SetIsItemSpawned(true); possessableComponent->SetPossessor(m_Parent->GetObjectID()); } auto* destroyableComponent = mount->GetComponent(); if (destroyableComponent) destroyableComponent->SetIsImmune(true); Game::entityManager->ConstructEntity(mount); possessorComponent->Mount(mount); GameMessages::MarkInventoryItemAsActive markActive; markActive.target = m_Parent->GetObjectID(); markActive.bActive = true; markActive.iType = eUnequippableActiveType::MOUNT; markActive.itemID = item->GetId(); markActive.Send(m_Parent->GetSystemAddress()); } void InventoryComponent::ApplyBuff(Item* item) const { const auto buffs = FindBuffs(item, true); for (const auto buff : buffs) { SkillComponent::HandleUnmanaged(buff, m_Parent->GetObjectID()); } } // Uncasting the item's equip skills sends RemoveBuff (with bFromUnEquip) for the buffs they applied, see ApplyBuffBehavior::UnCast. void InventoryComponent::RemoveBuff(Item* item) const { const auto buffs = FindBuffs(item, false); for (const auto buff : buffs) { SkillComponent::HandleUnCast(buff, m_Parent->GetObjectID()); } } void InventoryComponent::PushEquippedItems() { m_Pushed = m_Equipped; m_Dirty = true; } void InventoryComponent::PopEquippedItems() { const auto current = m_Equipped; for (const auto& pair : current) { auto* const item = FindItemById(pair.second.id); if (item == nullptr) { continue; } item->UnEquip(); } for (const auto& pair : m_Pushed) { auto* const item = FindItemById(pair.second.id); if (item == nullptr) { continue; } item->Equip(); } m_Pushed.clear(); auto destroyableComponent = m_Parent->GetComponent(); // Reset stats to full if (destroyableComponent) { destroyableComponent->SetHealth(static_cast(destroyableComponent->GetMaxHealth())); destroyableComponent->SetArmor(static_cast(destroyableComponent->GetMaxArmor())); destroyableComponent->SetImagination(static_cast(destroyableComponent->GetMaxImagination())); Game::entityManager->SerializeEntity(m_Parent); } m_Dirty = true; } bool InventoryComponent::IsEquipped(const LOT lot) const { for (const auto& pair : m_Equipped) { if (pair.second.lot == lot) { return true; } } return false; } void InventoryComponent::CheckItemSet(const LOT lot) { // Check if the lot is in the item set cache if (std::find(m_ItemSetsChecked.begin(), m_ItemSetsChecked.end(), lot) != m_ItemSetsChecked.end()) { return; } const std::string lot_query = "%" + std::to_string(lot) + "%"; auto query = CDClientDatabase::CreatePreppedStmt( "SELECT setID FROM ItemSets WHERE itemIDs LIKE ?;"); query.bind(1, lot_query.c_str()); auto result = query.execQuery(); while (!result.eof()) { const auto id = result.getIntField("setID"); bool found = false; // Check if we have the set already for (auto* itemset : m_Itemsets) { if (itemset->GetID() == id) { found = true; break; } } if (!found) { auto* set = new ItemSet(id, this); m_Itemsets.push_back(set); } result.nextRow(); } m_ItemSetsChecked.push_back(lot); result.finalize(); } void InventoryComponent::SetConsumable(LOT lot) { m_Consumable = lot; } LOT InventoryComponent::GetConsumable() const { return m_Consumable; } void InventoryComponent::AddItemSkills(const LOT lot) { const auto info = Inventory::FindItemComponent(lot); const auto slot = FindBehaviorSlot(info.equipLocation); if (slot == BehaviorSlot::Invalid) { return; } const auto skill = FindSkill(lot); SetSkill(slot, skill); } void InventoryComponent::FixInvisibleItems() { const auto numberItemsLoadedPerFrame = 12.0f; const auto callbackTime = 0.125f; const auto arbitaryInventorySize = 300.0f; // max in live + dlu is less than 300, seems like a good number. auto* const items = GetInventory(eInventoryType::ITEMS); if (!items) return; // Add an extra update to make sure the client can see all the items. const auto something = static_cast(std::ceil(items->GetItems().size() / arbitaryInventorySize)) + 1; LOG_DEBUG("Fixing invisible items with %i updates", something); for (int32_t i = 1; i < something + 1; i++) { // client loads 12 items every 1/8 seconds, we're adding a small hack to fix invisible inventory items due to closing the news screen too fast. m_Parent->AddCallbackTimer((arbitaryInventorySize / numberItemsLoadedPerFrame) * callbackTime * i, [this]() { GameMessages::UpdateInventoryUi updateUi; updateUi.target = m_Parent->GetObjectID(); updateUi.SendToClient(m_Parent->GetSystemAddress()); }); } } void InventoryComponent::RemoveItemSkills(const LOT lot) { const auto info = Inventory::FindItemComponent(lot); const auto slot = FindBehaviorSlot(info.equipLocation); if (slot == BehaviorSlot::Invalid) { return; } const auto index = m_Skills.find(slot); if (index == m_Skills.end()) { return; } const auto skillId = FindSkill(lot); // Only act on this slot if it still holds the skill from this item. // Another item may have overwritten the slot since this one was equipped. if (index->second != skillId) { return; } m_Skills.erase(slot); // Find another slot that still holds this skillID (if any). const auto surviving = std::ranges::find_if(m_Skills, [skillId](const auto& pair) { return pair.second == skillId; }); // The client stores one acquiredSkillsInfo entry per skillID, tagged with the slotID // it was originally added with. Always send RemoveSkill to clear that entry, then // re-add with the surviving slot so the client shows it in the correct place. GameMessages::RemoveSkill removeSkill; removeSkill.target = m_Parent->GetObjectID(); removeSkill.skillID = skillId; removeSkill.SendToClient(m_Parent->GetSystemAddress()); if (surviving != m_Skills.end()) { GameMessages::AddSkill addSkill; addSkill.target = m_Parent->GetObjectID(); addSkill.skillID = skillId; addSkill.slotID = surviving->first; addSkill.SendToClient(m_Parent->GetSystemAddress()); } if (slot == BehaviorSlot::Primary) { m_Skills.insert_or_assign(BehaviorSlot::Primary, 1); GameMessages::AddSkill addSkill; addSkill.target = m_Parent->GetObjectID(); addSkill.skillID = 1; addSkill.slotID = BehaviorSlot::Primary; addSkill.SendToClient(m_Parent->GetSystemAddress()); } } void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target) { for (auto* set : m_Itemsets) { set->TriggerPassiveAbility(trigger, target); } } void InventoryComponent::DespawnPet() { auto* current = PetComponent::GetActivePet(m_Parent->GetObjectID()); if (current != nullptr) { current->Deactivate(); } } void InventoryComponent::SpawnPet(Item* item) { auto* current = PetComponent::GetActivePet(m_Parent->GetObjectID()); if (current != nullptr) { current->Deactivate(); if (current->GetDatabaseId() == item->GetSubKey()) { return; } } // First check if we can summon the pet. You need 1 imagination to do so. auto destroyableComponent = m_Parent->GetComponent(); if (Game::config->GetValue("pets_take_imagination") == "1" && destroyableComponent && destroyableComponent->GetImagination() <= 0) { GameMessages::UseItemRequirementsResponse requirementsResponse; requirementsResponse.target = m_Parent->GetObjectID(); requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet; requirementsResponse.SendToClient(m_Parent->GetSystemAddress()); return; } EntityInfo info{}; info.lot = item->GetLot(); info.pos = m_Parent->GetPosition(); info.rot = QuatUtils::IDENTITY; info.spawnerID = m_Parent->GetObjectID(); auto* pet = Game::entityManager->CreateEntity(info); auto* petComponent = pet->GetComponent(); if (petComponent != nullptr) { petComponent->Activate(item); } Game::entityManager->ConstructEntity(pet); } void InventoryComponent::SetDatabasePet(LWOOBJID id, const DatabasePet& data) { m_Pets.insert_or_assign(id, data); } const DatabasePet& InventoryComponent::GetDatabasePet(LWOOBJID id) const { const auto& pair = m_Pets.find(id); if (pair == m_Pets.end()) return DATABASE_PET_INVALID; return pair->second; } bool InventoryComponent::IsPet(LWOOBJID id) const { const auto& pair = m_Pets.find(id); return pair != m_Pets.end(); } void InventoryComponent::RemoveDatabasePet(LWOOBJID id) { m_Pets.erase(id); } BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocation) { // Skill slot is determined by equipLocation, not itemType. // Mapping confirmed against live captures and client data (issue #1339). if (equipLocation == "special_r") return BehaviorSlot::Primary; if (equipLocation == "hair") return BehaviorSlot::Head; if (equipLocation == "special_l") return BehaviorSlot::Offhand; if (equipLocation == "clavicle") return BehaviorSlot::Neck; return BehaviorSlot::Invalid; } bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) { return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB || type == BRICKS_IN_BBB; } uint32_t InventoryComponent::FindSkill(const LOT lot) { auto* table = CDClientManager::GetTable(); const auto results = table->Query([=](const CDObjectSkills& entry) { return entry.objectTemplate == static_cast(lot); }); for (const auto& result : results) { if (result.castOnType == 0) { return result.skillID; } } return 0; } std::vector InventoryComponent::FindBuffs(Item* item, bool castOnEquip) const { std::vector buffs; if (item == nullptr) return buffs; auto* table = CDClientManager::GetTable(); auto* behaviors = CDClientManager::GetTable(); const auto results = table->Query([=](const CDObjectSkills& entry) { return entry.objectTemplate == static_cast(item->GetLot()); }); auto* missions = static_cast(m_Parent->GetComponent(eReplicaComponentType::MISSION)); for (const auto& result : results) { if (result.castOnType == 1) { const auto entry = behaviors->GetSkillByID(result.skillID); if (entry.skillID == 0) { LOG("Failed to find buff behavior for skill (%i)!", result.skillID); continue; } if (missions != nullptr && castOnEquip) { missions->Progress(eMissionTaskType::USE_SKILL, result.skillID); } // If item is not a proxy, add its buff to the added buffs. if (item->GetParent() == LWOOBJID_EMPTY) buffs.push_back(static_cast(entry.behaviorID)); } } return buffs; } void InventoryComponent::SetNPCItems(const std::vector& items) { m_Equipped.clear(); auto slot = 0u; for (const auto& item : items) { const LWOOBJID id = ObjectIDManager::GenerateObjectID(); const auto& info = Inventory::FindItemComponent(item); UpdateSlot(info.equipLocation, { id, static_cast(item), 1, slot++ }, true); } Game::entityManager->SerializeEntity(m_Parent); } InventoryComponent::~InventoryComponent() { for (const auto& inventory : m_Inventories) { delete inventory.second; } m_Inventories.clear(); for (auto* set : m_Itemsets) { delete set; } m_Itemsets.clear(); m_Pets.clear(); } std::vector InventoryComponent::GenerateProxies(Item* parent) { std::vector proxies; auto subItems = parent->GetInfo().subItems; if (subItems.empty()) { return proxies; } subItems.erase(std::remove_if(subItems.begin(), subItems.end(), ::isspace), subItems.end()); std::stringstream stream(subItems); std::string segment; std::vector lots; while (std::getline(stream, segment, ',')) { try { lots.push_back(std::stoi(segment)); } catch (std::invalid_argument& exception) { LOG("Failed to parse proxy (%s): (%s)!", segment.c_str(), exception.what()); } } for (const auto lot : lots) { if (!Inventory::IsValidItem(lot)) { continue; } auto* inventory = GetInventory(ITEM_SETS); auto* proxy = new Item(lot, inventory, inventory->FindEmptySlot(), 1, {}, parent->GetId(), false); EquipItem(proxy); proxies.push_back(proxy); } return proxies; } std::vector InventoryComponent::FindProxies(const LWOOBJID parent) { auto* inventory = GetInventory(ITEM_SETS); std::vector proxies; for (const auto& pair : inventory->GetItems()) { auto* item = pair.second; if (item->GetParent() == parent) { proxies.push_back(item); } } return proxies; } bool InventoryComponent::IsValidProxy(const LWOOBJID parent) { for (const auto& pair : m_Inventories) { const auto items = pair.second->GetItems(); for (const auto& candidate : items) { auto* item = candidate.second; if (item->GetId() == parent) { return true; } } } return false; } bool InventoryComponent::IsParentValid(Item* root) { if (root->GetInfo().subItems.empty()) { return true; } const auto id = root->GetId(); for (const auto& pair : m_Inventories) { const auto items = pair.second->GetItems(); for (const auto& candidate : items) { auto* item = candidate.second; if (item->GetParent() == id) { return true; } } } return false; } void InventoryComponent::CheckProxyIntegrity() { std::vector dead; for (const auto& pair : m_Inventories) { const auto& items = pair.second->GetItems(); for (const auto& candidate : items) { auto* item = candidate.second; const auto parent = item->GetParent(); if (parent == LWOOBJID_EMPTY) { continue; } if (IsValidProxy(parent)) { continue; } dead.push_back(item); } } for (auto* item : dead) { item->RemoveFromInventory(); } dead.clear(); /* for (const auto& pair : inventories) { const auto& items = pair.second->GetItems(); for (const auto& candidate : items) { auto* item = candidate.second; const auto parent = item->GetParent(); if (parent != LWOOBJID_EMPTY) { continue; } if (!item->IsEquipped()) { continue; } if (IsParentValid(item)) { continue; } dead.push_back(item); } } for (auto* item : dead) { item->RemoveFromInventory(); } */ } void InventoryComponent::PurgeProxies(Item* item) { const auto root = item->GetParent(); if (root != LWOOBJID_EMPTY) { Item* itemRoot = FindItemById(root); if (itemRoot != nullptr) { UnEquipItem(itemRoot); } return; } auto proxies = FindProxies(item->GetId()); for (auto* proxy : proxies) { proxy->UnEquip(); proxy->RemoveFromInventory(); } } void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { auto* petInventoryElement = document.FirstChildElement("obj")->FirstChildElement("pet"); if (petInventoryElement == nullptr) { m_Pets.clear(); return; } auto* petElement = petInventoryElement->FirstChildElement(); while (petElement != nullptr) { LWOOBJID id{}; LOT lot{}; int32_t moderationStatus{}; petElement->QueryAttribute("id", &id); petElement->QueryAttribute("l", &lot); petElement->QueryAttribute("m", &moderationStatus); const char* name = petElement->Attribute("n"); DatabasePet databasePet; databasePet.lot = lot; databasePet.moderationState = moderationStatus; databasePet.name = name ? name : ""; SetDatabasePet(id, databasePet); // Pets named before pet_names recorded their LOT get it now that their owner is loaded if (id != LWOOBJID_EMPTY && lot > 0) Database::Get()->SetPetLotIfMissing(id, lot); petElement = petElement->NextSiblingElement(); } } void InventoryComponent::UpdatePetXml(tinyxml2::XMLDocument& document) { auto* petInventoryElement = document.FirstChildElement("obj")->FirstChildElement("pet"); if (petInventoryElement == nullptr) { petInventoryElement = document.NewElement("pet"); document.FirstChildElement("obj")->LinkEndChild(petInventoryElement); } petInventoryElement->DeleteChildren(); for (const auto& pet : m_Pets) { auto* petElement = document.NewElement("p"); petElement->SetAttribute("id", pet.first); petElement->SetAttribute("l", pet.second.lot); petElement->SetAttribute("m", pet.second.moderationState); petElement->SetAttribute("n", pet.second.name.c_str()); petElement->SetAttribute("t", 0); petInventoryElement->LinkEndChild(petElement); } } bool InventoryComponent::SetSkill(int32_t slot, uint32_t skillId) { BehaviorSlot behaviorSlot = BehaviorSlot::Invalid; if (slot == 1) behaviorSlot = BehaviorSlot::Primary; else if (slot == 2) behaviorSlot = BehaviorSlot::Offhand; else if (slot == 3) behaviorSlot = BehaviorSlot::Neck; else if (slot == 4) behaviorSlot = BehaviorSlot::Head; else if (slot == 5) behaviorSlot = BehaviorSlot::Consumable; else return false; return SetSkill(behaviorSlot, skillId); } bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) { if (skillId == 0) return false; const auto index = m_Skills.find(slot); if (index != m_Skills.end()) { const auto old = index->second; // Only remove the old skill from the client if no other slot still holds it. // The client's acquiredSkillsInfo is keyed by skillID (one entry per skill), // so RemoveSkill clears it globally — sending it while another slot still uses // the same skillID would break that slot on the client. const auto usedElsewhere = std::ranges::any_of(m_Skills, [&](const auto& pair) { return pair.first != slot && pair.second == old; }); if (!usedElsewhere) { GameMessages::RemoveSkill removeSkill; removeSkill.target = m_Parent->GetObjectID(); removeSkill.skillID = old; removeSkill.SendToClient(m_Parent->GetSystemAddress()); } } // Only send AddSkill if the client doesn't already know about this skillID. // The client early-exits on duplicate AddSkill (same skillID already in // acquiredSkillsInfo) without updating the slot — so only send when it's new. const auto alreadyKnown = std::ranges::any_of(m_Skills, [&](const auto& pair) { return pair.first != slot && pair.second == skillId; }); if (!alreadyKnown) { GameMessages::AddSkill addSkill; addSkill.target = m_Parent->GetObjectID(); addSkill.skillID = skillId; addSkill.slotID = slot; addSkill.SendToClient(m_Parent->GetSystemAddress()); } m_Skills.insert_or_assign(slot, skillId); return true; } void InventoryComponent::UpdateGroup(const GroupUpdate& groupUpdate) { if (groupUpdate.groupId.empty()) return; if (groupUpdate.inventory != eInventoryType::BRICKS && groupUpdate.inventory != eInventoryType::MODELS) { LOG("Invalid inventory type for grouping %s", StringifiedEnum::ToString(groupUpdate.inventory).data()); return; } auto& groups = m_Groups[groupUpdate.inventory]; auto groupItr = std::ranges::find_if(groups, [&groupUpdate](const Group& group) { return group.groupId == groupUpdate.groupId; }); if (groupUpdate.command != GroupUpdateCommand::ADD && groupItr == groups.end()) { LOG("Group %s not found in inventory %s. Cannot process command.", groupUpdate.groupId.c_str(), StringifiedEnum::ToString(groupUpdate.inventory).data()); return; } if (groupUpdate.command == GroupUpdateCommand::ADD && groups.size() >= MaximumGroupCount) { LOG("Cannot add group to inventory %s. Maximum group count reached.", StringifiedEnum::ToString(groupUpdate.inventory).data()); return; } switch (groupUpdate.command) { case GroupUpdateCommand::ADD: { auto& group = groups.emplace_back(); group.groupId = groupUpdate.groupId; group.groupName = groupUpdate.groupName; break; } case GroupUpdateCommand::ADD_LOT: { groupItr->lots.insert(groupUpdate.lot); break; } case GroupUpdateCommand::REMOVE: { groups.erase(groupItr); break; } case GroupUpdateCommand::REMOVE_LOT: { groupItr->lots.erase(groupUpdate.lot); break; } case GroupUpdateCommand::MODIFY: { groupItr->groupName = groupUpdate.groupName; break; } default: { LOG("Invalid group update command %i", groupUpdate.command); break; } } } void InventoryComponent::UpdateGroupXml(tinyxml2::XMLElement& groups) const { for (const auto& [inventory, groupsData] : m_Groups) { for (const auto& group : groupsData) { auto* const groupElement = groups.InsertNewChildElement("grp"); groupElement->SetAttribute("id", group.groupId.c_str()); groupElement->SetAttribute("n", group.groupName.c_str()); groupElement->SetAttribute("t", static_cast(inventory)); groupElement->SetAttribute("u", 0); std::ostringstream lots; bool first = true; for (const auto lot : group.lots) { if (!first) lots << ' '; first = false; lots << lot; } groupElement->SetAttribute("l", lots.str().c_str()); } } } void InventoryComponent::LoadGroupXml(const tinyxml2::XMLElement& groups) { auto* groupElement = groups.FirstChildElement("grp"); while (groupElement) { const char* groupId = nullptr; const char* groupName = nullptr; const char* lots = nullptr; uint32_t inventory = eInventoryType::INVALID; groupElement->QueryStringAttribute("id", &groupId); groupElement->QueryStringAttribute("n", &groupName); groupElement->QueryStringAttribute("l", &lots); groupElement->QueryAttribute("t", &inventory); if (!groupId || !groupName || !lots) { LOG("Failed to load group from xml id %i name %i lots %i", groupId == nullptr, groupName == nullptr, lots == nullptr); } else { auto& group = m_Groups[static_cast(inventory)].emplace_back(); group.groupId = groupId; group.groupName = groupName; for (const auto& lotStr : GeneralUtils::SplitString(lots, ' ')) { auto lot = GeneralUtils::TryParse(lotStr); if (lot) group.lots.insert(*lot); } } groupElement = groupElement->NextSiblingElement("grp"); } } void InventoryComponent::RegenerateItemIDs() { for (auto* const inventory : m_Inventories | std::views::values) { inventory->RegenerateItemIDs(); } } std::string DebugInvToString(const eInventoryType inv, bool verbose) { switch (inv) { case ITEMS: return "Backpack"; case VAULT_ITEMS: return "Bank"; case BRICKS: return verbose ? "Bricks" : "Bricks (contents only shown in high-detail report)"; case MODELS_IN_BBB: return "Models in BBB"; case TEMP_ITEMS: return "Temp Equip"; case MODELS: return verbose ? "Model" : "Model (contents only shown in high-detail report)"; case TEMP_MODELS: return "Module"; case BEHAVIORS: return "B3 Behavior"; case PROPERTY_DEEDS: return "Property"; case BRICKS_IN_BBB: return "Brick In BBB"; case VENDOR: return "Vendor"; case VENDOR_BUYBACK: return "BuyBack"; case QUEST: return "Quest"; case DONATION: return "Donation"; case VAULT_MODELS: return "Bank Model"; case ITEM_SETS: return "Bank Behavior"; case INVALID: return "Invalid"; case ALL: return "All"; } return ""; } bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& cmpt = reportInfo.info->PushDebug("Inventory"); cmpt.PushDebug("Component ID") = GetComponentID(); uint32_t numItems = 0; for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size(); cmpt.PushDebug("Inventory Item Count") = numItems; auto& itemsInBags = cmpt.PushDebug("Items in bags"); for (const auto& [id, inventoryMut] : m_Inventories) { if (!inventoryMut) continue; const auto* const inventory = inventoryMut; auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, reportInfo.bVerbose) + " - " + std::to_string(id)); for (uint32_t i = 0; i < inventory->GetSize(); i++) { const auto* const item = inventory->FindItemBySlot(i); if (!item) continue; std::stringstream ss; ss << "%[Objects_" << item->GetLot() << "_name] Slot " << item->GetSlot(); auto& slot = curInv.PushDebug(ss.str()); slot.PushDebug("Object ID", "LWOOBJID") = std::to_string(item->GetId()); slot.PushDebug("LOT", "LOT") = item->GetLot(); if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug("Subkey", "LWOOBJID") = std::to_string(item->GetSubKey()); slot.PushDebug("Count") = item->GetCount(); slot.PushDebug("Slot") = item->GetSlot(); slot.PushDebug("Bind on pickup") = item->GetInfo().isBOP; slot.PushDebug("Bind on equip") = item->GetInfo().isBOE; slot.PushDebug("Is currently bound") = item->GetBound(); auto& extra = slot.PushDebug("Extra Info"); for (const auto& setting : item->GetConfig().values | std::views::values) { if (setting) extra.PushDebug(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } } auto& equipped = cmpt.PushDebug("Equipped Items"); for (const auto& [location, info] : GetEquippedItems()) { std::stringstream ss; ss << "%[Objects_" << info.lot << "_name]"; auto& equipSlot = equipped.PushDebug(ss.str()); equipSlot.PushDebug("Location") = location; equipSlot.PushDebug("Object ID", "LWOOBJID") = std::to_string(info.id); equipSlot.PushDebug("LOT", "LOT") = info.lot; equipSlot.PushDebug("Slot") = info.slot; equipSlot.PushDebug("Count") = info.count; auto& extra = equipSlot.PushDebug("Extra Info"); for (const auto& setting : info.config.values | std::views::values) { if (setting) extra.PushDebug(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } return true; } void InventoryComponent::OnEquipInventory(const GameMessages::EquipInventory& msg) { Item* item = FindItemById(msg.itemToEquip); if (!item) return; item->Equip(); Game::entityManager->SerializeEntity(m_Parent); } void InventoryComponent::OnUnEquipInventory(const GameMessages::UnEquipInventory& msg) { auto* item = FindItemById(msg.itemToUnequip); if (!item) return; item->UnEquip(); Game::entityManager->SerializeEntity(m_Parent); } void InventoryComponent::OnRemoveItemFromInventory(const GameMessages::RemoveItemFromInventory& msg) { // this is used for a lot more than just inventory trashing (trades, vendors, etc.) but for now since it's just used for that, that's all im going to implement auto* item = FindItemById(msg.iObjID); if (item == nullptr) { return; } const auto iStackCount = std::min(item->GetCount(), msg.iStackCount); if (msg.bConfirmed && !CanDelete(*item)) { LOG("%llu tried to delete %llu (LOT %i), which its deletion restriction does not allow", m_Parent->GetObjectID(), item->GetId(), item->GetLot()); return; } if (msg.bConfirmed) { const auto itemType = static_cast(item->GetInfo().itemType); if (itemType == eItemType::MODEL || itemType == eItemType::LOOT_MODEL) { item->DisassembleModel(iStackCount); } else if (itemType == eItemType::VEHICLE) { Database::Get()->DeleteUgcBuild(item->GetSubKey()); } auto lot = item->GetLot(); item->SetCount(item->GetCount() - iStackCount, true); Game::entityManager->SerializeEntity(m_Parent); auto* missionComponent = m_Parent->GetComponent(); if (missionComponent != nullptr) { missionComponent->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -iStackCount); } } } void InventoryComponent::OnMoveItemInInventory(const GameMessages::MoveItemInInventory& msg) { auto* item = FindItemById(msg.iObjID); if (!item) { return; } MoveStack(item, static_cast(msg.destInvType), msg.slot); Game::entityManager->SerializeEntity(m_Parent); } void InventoryComponent::OnMoveItemBetweenInventoryTypes(const GameMessages::MoveItemBetweenInventoryTypes& msg) { auto showFlyingLoot = msg.showFlyingLoot; auto* item = FindItemById(msg.objectID); if (!item) { // Attempt to find the item by lot in inventory A since A is the source inventory. item = FindItemByLot(msg.templateID, static_cast(msg.inventoryTypeA)); if (!item) { // As a final resort, try to find the item in its default inventory based on type. item = FindItemByLot(msg.templateID); if (!item) { return; } } } if (m_Parent->GetCharacter()) { if (m_Parent->GetCharacter()->GetBuildMode()) { showFlyingLoot = false; } } MoveItemToInventory(item, msg.inventoryTypeB, msg.stackCount, showFlyingLoot); Game::entityManager->SerializeEntity(m_Parent); } void InventoryComponent::OnRequestMoveItemBetweenInventoryTypes(const GameMessages::RequestMoveItemBetweenInventoryTypes& msg, const SystemAddress& sysAddr) { GameMessages::ResponseMoveItemBetweenInventoryTypes response; response.target = m_Parent->GetObjectID(); response.inventoryTypeDestination = msg.invTypeDst; response.inventoryTypeSource = msg.invTypeSrc; if (msg.itemID != LWOOBJID_EMPTY) { auto* item = FindItemById(msg.itemID); if (!item) { response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_ITEM_NOT_FOUND; response.SendToClient(sysAddr); return; } if (item->GetLot() == 6086) { // Thinking hat response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_CANT_MOVE_THINKING_HAT; response.SendToClient(sysAddr); return; } auto* destInv = GetInventory(msg.invTypeDst); if (destInv && destInv->GetEmptySlots() == 0) { response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_INV_FULL; response.SendToClient(sysAddr); return; } // Despawn the pet if we are moving that pet to the vault. auto* petComponent = PetComponent::GetActivePet(m_Parent->GetObjectID()); if (petComponent && petComponent->GetDatabaseId() == item->GetSubKey()) { DespawnPet(); } MoveItemToInventory(item, msg.invTypeDst, msg.iStackCount, msg.showFlyingLoot, false, false, msg.destSlot); response.response = eReponseMoveItemBetweenInventoryTypeCode::SUCCESS; response.SendToClient(sysAddr); } } void InventoryComponent::OnClientItemConsumed(const GameMessages::ClientItemConsumed& msg) { auto* item = FindItemById(msg.item); if (item == nullptr) { return; } LOT itemLot = item->GetLot(); item->Consume(); auto* missions = static_cast(m_Parent->GetComponent(eReplicaComponentType::MISSION)); if (missions != nullptr) { missions->Progress(eMissionTaskType::USE_ITEM, itemLot); } } void InventoryComponent::OnUseNonEquipmentItem(const GameMessages::UseNonEquipmentItem& msg) { auto* item = FindItemById(msg.itemToUse); if (item) item->UseNonEquip(item); } void InventoryComponent::OnUpdateInventoryGroup(const GameMessages::UpdateInventoryGroup& msg) { GroupUpdate groupUpdate; groupUpdate.groupId = msg.groupID; groupUpdate.inventory = msg.inventoryType; groupUpdate.groupName = GeneralUtils::UTF16ToWTF8(msg.groupName); if (msg.action == "ADD") groupUpdate.command = GroupUpdateCommand::ADD; else if (msg.action == "MODIFY") groupUpdate.command = GroupUpdateCommand::MODIFY; else if (msg.action == "REMOVE") groupUpdate.command = GroupUpdateCommand::REMOVE; else { LOG("Invalid action %s", msg.action.c_str()); return; } UpdateGroup(groupUpdate); } void InventoryComponent::OnUpdateInventoryGroupContents(const GameMessages::UpdateInventoryGroupContents& msg) { GroupUpdate groupUpdate; if (msg.action == "ADD") groupUpdate.command = GroupUpdateCommand::ADD_LOT; else if (msg.action == "REMOVE") groupUpdate.command = GroupUpdateCommand::REMOVE_LOT; else { LOG("Invalid action %s", msg.action.c_str()); return; } groupUpdate.groupId = msg.groupID; groupUpdate.inventory = msg.inventoryType; groupUpdate.lot = msg.lot; UpdateGroup(groupUpdate); } namespace { // A comma separated list of numbers; anything that is not a number is skipped std::vector ParseDeletionRestrictionIds(const std::string& ids) { std::vector parsed; for (auto part : GeneralUtils::SplitString(ids, ',')) { const auto first = part.find_first_not_of(" \t\r\n"); if (first == std::string::npos) continue; part = part.substr(first, part.find_last_not_of(" \t\r\n") - first + 1); const auto value = GeneralUtils::TryParse(part); if (value) parsed.push_back(*value); } return parsed; } } bool InventoryComponent::CanDelete(const Item& item) const { if (m_Parent->GetGMLevel() == eGameMasterLevel::OPERATOR) return true; const auto row = static_cast(item.GetInfo().delResIndex); if (row < 0) return true; std::set visited; return CheckDeletionRestriction(item.GetId(), row, visited); } bool InventoryComponent::CheckDeletionRestriction(const LWOOBJID itemId, const int32_t row, std::set& visited) const { // Rows that are missing, not restricted, of an unknown type or without IDs don't restrict anything const auto* const restriction = CDClientManager::GetTable()->Get(row); if (!restriction || !restriction->restricted) return true; const auto checkType = static_cast(restriction->checkType); if (checkType == eDeletionRestrictionCheckType::ALWAYS_RESTRICTED) return false; if (restriction->checkType < 0 || restriction->checkType > GeneralUtils::ToUnderlying(eDeletionRestrictionCheckType::ALWAYS_RESTRICTED)) return true; const auto ids = ParseDeletionRestrictionIds(restriction->ids); if (ids.empty()) return true; switch (checkType) { case eDeletionRestrictionCheckType::ANY_RESTRICTION: case eDeletionRestrictionCheckType::ALL_RESTRICTIONS: { if (!visited.insert(row).second) return true; const bool any = checkType == eDeletionRestrictionCheckType::ANY_RESTRICTION; bool result = !any; for (const auto id : ids) { const bool allowed = CheckDeletionRestriction(itemId, static_cast(id), visited); result = any ? (result || allowed) : (result && allowed); } visited.erase(row); return result; } case eDeletionRestrictionCheckType::ZONE: { const auto mapId = Game::zoneManager->GetZoneID().GetMapID(); return std::ranges::find(ids, static_cast(mapId)) != ids.end(); } case eDeletionRestrictionCheckType::LOTS_INCLUDED: case eDeletionRestrictionCheckType::LOTS_EXCLUDED: { // The other items the player has, in any inventory, not counting the one being deleted std::set missing(ids.begin(), ids.end()); for (const auto& [type, inventory] : m_Inventories) { for (const auto& [id, other] : inventory->GetItems()) { if (id == itemId || !missing.contains(other->GetLot())) continue; if (checkType == eDeletionRestrictionCheckType::LOTS_INCLUDED) return true; missing.erase(other->GetLot()); if (missing.empty()) return true; } } return false; } default: return true; } }