mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1985 lines
57 KiB
C++
1985 lines
57 KiB
C++
#include "InventoryComponent.h"
|
|
#include "EconomyLedger.h"
|
|
|
|
#include <sstream>
|
|
|
|
#include "Entity.h"
|
|
#include "Item.h"
|
|
#include "Game.h"
|
|
#include "dServer.h"
|
|
#include "Logger.h"
|
|
#include "CDClientManager.h"
|
|
#include "ObjectIDManager.h"
|
|
#include "MissionComponent.h"
|
|
#include "GameMessages.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 <ranges>
|
|
|
|
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<CDComponentsRegistryTable>();
|
|
const auto componentId = compRegistryTable->GetByIDAndType(lot, eReplicaComponentType::INVENTORY);
|
|
|
|
auto* inventoryComponentTable = CDClientManager::GetTable<CDInventoryComponentTable>();
|
|
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<LOT>(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<LOT>(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<eInventoryType, Inventory*>& 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);
|
|
}
|
|
|
|
auto* missions = static_cast<MissionComponent*>(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<uint32_t>(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<uint32_t>(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::SendAddItemToInventoryClientSync(m_Parent, m_Parent->GetSystemAddress(), item, id, options.showFlyingLoot && !options.equip, static_cast<int>(count), subKey, lootSourceType);
|
|
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(lootSourceType));
|
|
if (options.equip) {
|
|
item->Equip();
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
auto* missions = m_Parent->GetComponent<MissionComponent>();
|
|
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<LWOOBJID, uint32_t> 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<int64_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<int64_t>(moved), static_cast<uint32_t>(eLootSourceType::VENDOR));
|
|
} else if (originType == eInventoryType::VENDOR_BUYBACK && inventory != eInventoryType::VENDOR_BUYBACK) {
|
|
EconomyLedger::RecordItemsUnsuppressed(m_Parent, lot, moved, static_cast<uint32_t>(eLootSourceType::VENDOR));
|
|
}
|
|
|
|
auto* missionComponent = m_Parent->GetComponent<MissionComponent>();
|
|
|
|
if (missionComponent != nullptr) {
|
|
if (IsTransferInventory(inventory)) {
|
|
missionComponent->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -static_cast<int32_t>(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<eInventoryType, int32_t> 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<uint32_t>(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<double>(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::SendNotifyNotEnoughInvSpace(m_Parent->GetObjectID(), slotsNeeded, ITEMS, m_Parent->GetSystemAddress());
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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<eInventoryType>(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);
|
|
|
|
auto* inventory = GetInventory(static_cast<eInventoryType>(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
|
|
|
|
auto* item = new Item(id, lot, inventory, slot, count, bound, {}, parent, subKey);
|
|
|
|
item->LoadConfigXml(*itemElement);
|
|
|
|
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<Inventory*> inventoriesToSave;
|
|
|
|
// Need to prevent some transfer inventories from being saved
|
|
for (const auto& pair : this->m_Inventories) {
|
|
auto* inventory = pair.second;
|
|
|
|
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, MODELS_IN_BBB, 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<unsigned int>(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<unsigned int>(item->GetSlot()));
|
|
itemElement->SetAttribute("c", static_cast<unsigned int>(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<uint32_t>(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<uint16_t>(item.slot);
|
|
|
|
outBitStream.Write0();
|
|
|
|
bool flag = !item.config.values.empty();
|
|
outBitStream.Write(flag);
|
|
if (flag) {
|
|
RakNet::BitStream ldfStream;
|
|
ldfStream.Write<int32_t>(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<std::u16string> ldf_data(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr));
|
|
ldf_data.WriteToPacket(ldfStream);
|
|
} else {
|
|
data->WriteToPacket(ldfStream);
|
|
}
|
|
}
|
|
outBitStream.Write(ldfStream.GetNumberOfBytesUsed() + 1);
|
|
outBitStream.Write<uint8_t>(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<CharacterComponent>();
|
|
|
|
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<ProximityMonitorComponent>();
|
|
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<eItemType>(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<CDComponentsRegistryTable>();
|
|
if (!compRegistryTable) return;
|
|
int32_t scriptComponentID = compRegistryTable->GetByIDAndType(equippedItem->GetLot(), eReplicaComponentType::SCRIPT, -1);
|
|
if (scriptComponentID > -1) {
|
|
CDScriptComponentTable* scriptCompTable = CDClientManager::GetTable<CDScriptComponentTable>();
|
|
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<CDComponentsRegistryTable>();
|
|
if (!compRegistryTable) return;
|
|
int32_t scriptComponentID = compRegistryTable->GetByIDAndType(unequippedItem->GetLot(), eReplicaComponentType::SCRIPT, -1);
|
|
if (scriptComponentID > -1) {
|
|
CDScriptComponentTable* scriptCompTable = CDClientManager::GetTable<CDScriptComponentTable>();
|
|
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<PossessorComponent>();
|
|
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::SendSetMountInventoryID(m_Parent, item->GetId(), 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<PossessableComponent>();
|
|
if (possessableComponent) {
|
|
possessableComponent->SetIsItemSpawned(true);
|
|
possessableComponent->SetPossessor(m_Parent->GetObjectID());
|
|
}
|
|
|
|
auto* destroyableComponent = mount->GetComponent<DestroyableComponent>();
|
|
if (destroyableComponent) destroyableComponent->SetIsImmune(true);
|
|
|
|
Game::entityManager->ConstructEntity(mount);
|
|
possessorComponent->Mount(mount);
|
|
|
|
GameMessages::SendMarkInventoryItemAsActive(m_Parent->GetObjectID(), true, eUnequippableActiveType::MOUNT, item->GetId(), 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());
|
|
}
|
|
}
|
|
|
|
// TODO Something needs to send the remove buff GameMessage as well when it is unequipping items that would remove buffs.
|
|
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<DestroyableComponent>();
|
|
|
|
// Reset stats to full
|
|
if (destroyableComponent) {
|
|
destroyableComponent->SetHealth(static_cast<int32_t>(destroyableComponent->GetMaxHealth()));
|
|
destroyableComponent->SetArmor(static_cast<int32_t>(destroyableComponent->GetMaxArmor()));
|
|
destroyableComponent->SetImagination(static_cast<int32_t>(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<int32_t>(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::SendUpdateInventoryUi(m_Parent->GetObjectID(), 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::SendRemoveSkill(m_Parent, skillId);
|
|
if (surviving != m_Skills.end()) {
|
|
GameMessages::SendAddSkill(m_Parent, skillId, surviving->first);
|
|
}
|
|
|
|
if (slot == BehaviorSlot::Primary) {
|
|
m_Skills.insert_or_assign(BehaviorSlot::Primary, 1);
|
|
GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
|
|
}
|
|
}
|
|
|
|
void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target) {
|
|
for (auto* set : m_Itemsets) {
|
|
set->TriggerPassiveAbility(trigger, target);
|
|
}
|
|
}
|
|
|
|
bool InventoryComponent::HasAnyPassive(const std::vector<eItemSetPassiveAbilityID>& passiveIDs, int32_t equipmentRequirement) const {
|
|
for (auto* set : m_Itemsets) {
|
|
if (set->GetEquippedCount() < equipmentRequirement) {
|
|
continue;
|
|
}
|
|
|
|
// Check if the set has any of the passive abilities
|
|
if (std::find(passiveIDs.begin(), passiveIDs.end(), static_cast<eItemSetPassiveAbilityID>(set->GetID())) != passiveIDs.end()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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<DestroyableComponent>();
|
|
|
|
if (Game::config->GetValue("pets_take_imagination") == "1" && destroyableComponent && destroyableComponent->GetImagination() <= 0) {
|
|
GameMessages::SendUseItemRequirementsResponse(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), eUseItemResponse::NoImaginationForPet);
|
|
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<PetComponent>();
|
|
|
|
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;
|
|
}
|
|
|
|
uint32_t InventoryComponent::FindSkill(const LOT lot) {
|
|
auto* table = CDClientManager::GetTable<CDObjectSkillsTable>();
|
|
|
|
const auto results = table->Query([=](const CDObjectSkills& entry) {
|
|
return entry.objectTemplate == static_cast<unsigned int>(lot);
|
|
});
|
|
|
|
for (const auto& result : results) {
|
|
if (result.castOnType == 0) {
|
|
return result.skillID;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
std::vector<uint32_t> InventoryComponent::FindBuffs(Item* item, bool castOnEquip) const {
|
|
std::vector<uint32_t> buffs;
|
|
if (item == nullptr) return buffs;
|
|
auto* table = CDClientManager::GetTable<CDObjectSkillsTable>();
|
|
auto* behaviors = CDClientManager::GetTable<CDSkillBehaviorTable>();
|
|
|
|
const auto results = table->Query([=](const CDObjectSkills& entry) {
|
|
return entry.objectTemplate == static_cast<unsigned int>(item->GetLot());
|
|
});
|
|
|
|
auto* missions = static_cast<MissionComponent*>(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<uint32_t>(entry.behaviorID));
|
|
}
|
|
}
|
|
|
|
return buffs;
|
|
}
|
|
|
|
void InventoryComponent::SetNPCItems(const std::vector<LOT>& 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<LOT>(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<Item*> InventoryComponent::GenerateProxies(Item* parent) {
|
|
std::vector<Item*> 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<uint32_t> 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<Item*> InventoryComponent::FindProxies(const LWOOBJID parent) {
|
|
auto* inventory = GetInventory(ITEM_SETS);
|
|
|
|
std::vector<Item*> 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<Item*> 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);
|
|
|
|
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::SendRemoveSkill(m_Parent, old);
|
|
}
|
|
}
|
|
|
|
// 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::SendAddSkill(m_Parent, skillId, slot);
|
|
}
|
|
|
|
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<uint32_t>(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<eInventoryType>(inventory)].emplace_back();
|
|
group.groupId = groupId;
|
|
group.groupName = groupName;
|
|
|
|
for (const auto& lotStr : GeneralUtils::SplitString(lots, ' ')) {
|
|
auto lot = GeneralUtils::TryParse<LOT>(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<AMFIntValue>("Component ID") = GetComponentID();
|
|
uint32_t numItems = 0;
|
|
for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size();
|
|
cmpt.PushDebug<AMFIntValue>("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<AMFStringValue>("Object ID", "LWOOBJID") = std::to_string(item->GetId());
|
|
slot.PushDebug<AMFIntValue>("LOT", "LOT") = item->GetLot();
|
|
if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug<AMFStringValue>("Subkey", "LWOOBJID") = std::to_string(item->GetSubKey());
|
|
slot.PushDebug<AMFIntValue>("Count") = item->GetCount();
|
|
slot.PushDebug<AMFIntValue>("Slot") = item->GetSlot();
|
|
slot.PushDebug<AMFBoolValue>("Bind on pickup") = item->GetInfo().isBOP;
|
|
slot.PushDebug<AMFBoolValue>("Bind on equip") = item->GetInfo().isBOE;
|
|
slot.PushDebug<AMFBoolValue>("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<AMFStringValue>(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<AMFStringValue>("Location") = location;
|
|
equipSlot.PushDebug<AMFStringValue>("Object ID", "LWOOBJID") = std::to_string(info.id);
|
|
equipSlot.PushDebug<AMFIntValue>("LOT", "LOT") = info.lot;
|
|
equipSlot.PushDebug<AMFIntValue>("Slot") = info.slot;
|
|
equipSlot.PushDebug<AMFIntValue>("Count") = info.count;
|
|
auto& extra = equipSlot.PushDebug("Extra Info");
|
|
for (const auto& setting : info.config.values | std::views::values) {
|
|
if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|