mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Live, on every equip and unequip (captures: 22,422 ChangeObjectWorldState, 4,269 equip effects, 245 UncastSkill): - ChangeObjectWorldState on the item to everyone: ATTACHED when equipped, INVENTORY when unequipped, proxies (sub-items) included. - The equip-<slot> / unequip-<slot> effect on the wearer to everyone (effect -1, priority 1.07), the slot picked from the item's equip location as the client's ItemComponent does (0x00cdafb0): special_l left, special_r right, hair head, clavicle, chest, legs; none for others (e.g. a rocket's Extra_1); the item's equipEffects with "equip"/"unequip" when it has one; no equip effect for noEquipAnimation items, no unequip effect for proxies. - Order when an item replaces another: the new item's state and effect, then the old item's, then the old item's proxies taken away (RemoveItemFromInventory, UnEquipInventory with ignoreCooldown, ChangeObjectWorldState INVENTORY, to the player), then UncastSkill for the old item's equip skills (castOnType 1, not its proxies'), then its skill removed and the new one added, then the new item's proxies. - An equipped item taken out of the inventory is followed by UnEquipInventory (ignoreCooldown) to the player. DLU sent none of these (only the rocket's ChangeObjectWorldState, which now comes from equipping it, before RocketEquipped as live). Check in game (with a second player watching): equip and unequip a hat, a sword, a shield, a shirt, pants and a backpack; both players see the equip animation/effect. Put a ninja hood on, then another hat over it: the hood's back piece disappears and its passive effects end (no lingering aura or stat). Launch a rocket: it is still shown during the launch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2411 lines
73 KiB
C++
2411 lines
73 KiB
C++
#include "InventoryComponent.h"
|
|
#include "BrickByBrick.h"
|
|
#include "Contraband.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 "CDDeletionRestrictionsTable.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include <set>
|
|
#include "ObjectIDManager.h"
|
|
#include "MissionComponent.h"
|
|
#include "GameMessages.h"
|
|
#include "InventoryMessages.h"
|
|
#include "Database.h"
|
|
#include "CDModularBuildComponentTable.h"
|
|
#include "SkillMessages.h"
|
|
#include <string_view>
|
|
#include <array>
|
|
#include "eObjectWorldState.h"
|
|
#include "EffectsMessages.h"
|
|
#include "ZoneMessages.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 <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);
|
|
}
|
|
|
|
Contraband::OnItemAdded(m_Parent, lot, count, lootSourceType, inventorySourceType);
|
|
|
|
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::AddItemToInventoryClientSync addItem;
|
|
addItem.target = m_Parent->GetObjectID();
|
|
addItem.SetItem(*item);
|
|
addItem.eLootTypeSource = lootSourceType;
|
|
addItem.iSubkey = subKey;
|
|
addItem.itemCount = static_cast<int>(count);
|
|
addItem.newObjID = id;
|
|
addItem.showFlyingLoot = options.showFlyingLoot && !options.equip;
|
|
addItem.SendToClient(m_Parent->GetSystemAddress());
|
|
if (auto* characterComponent = m_Parent->GetComponent<CharacterComponent>()) characterComponent->TrackItemsAdded(inventoryType, count);
|
|
EconomyLedger::RecordItems(m_Parent, lot, count, static_cast<uint32_t>(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<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::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<CDModularBuildComponentTable>()->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<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);
|
|
|
|
// 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<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
|
|
|
|
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<Inventory*> 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<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();
|
|
|
|
// Saved while the equipped items are pushed (build mode, possibly carrying a model): the character's equipment is
|
|
// the pushed state, which PopEquippedItems puts back, not the temporary items equipped since.
|
|
const auto& savedEquipment = m_HasPushed ? m_Pushed : m_Equipped;
|
|
const auto isSavedEquipped = [&savedEquipment](const LWOOBJID id) {
|
|
return std::ranges::any_of(savedEquipment, [id](const auto& slot) { return slot.second.id == id; });
|
|
};
|
|
|
|
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", isSavedEquipped(item->GetId()));
|
|
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);
|
|
|
|
// Live: the item is attached (and its effect played) first, then the item it replaces is unequipped, then the
|
|
// skills change and the item's proxies are added
|
|
SendEquipState(*item, true);
|
|
|
|
UpdateSlot(item->GetInfo().equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot(), item->GetConfig() });
|
|
|
|
ApplyBuff(item);
|
|
|
|
AddItemSkills(item->GetLot());
|
|
|
|
GenerateProxies(item);
|
|
|
|
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);
|
|
}
|
|
|
|
// Taken out of its slot first, so its proxies (purged below) don't unequip it again
|
|
RemoveSlot(item->GetInfo().equipLocation);
|
|
|
|
// Live: back to the inventory (and its effect), its proxies taken away, its equip skills uncast, then its skill
|
|
// removed
|
|
SendEquipState(*item, false);
|
|
|
|
PurgeProxies(item);
|
|
|
|
SendUncastEquipSkills(*item);
|
|
|
|
RemoveBuff(item);
|
|
|
|
RemoveItemSkills(item->GetLot());
|
|
|
|
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::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<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::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_HasPushed = true;
|
|
|
|
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();
|
|
m_HasPushed = false;
|
|
|
|
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::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<DestroyableComponent>();
|
|
|
|
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<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 || type == BRICKS_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) {
|
|
// Live told the client each proxy left the inventory (then UnEquipInventory and ChangeObjectWorldState,
|
|
// from RemoveFromInventory)
|
|
GameMessages::RemoveItemFromInventory removeItem;
|
|
removeItem.target = m_Parent->GetObjectID();
|
|
removeItem.eInvType = proxy->GetInventory()->GetType();
|
|
removeItem.eLootTypeSource = static_cast<int32_t>(eLootSourceType::INVENTORY);
|
|
removeItem.iObjID = proxy->GetId();
|
|
removeItem.iObjTemplate = proxy->GetLot();
|
|
removeItem.iRequestingObjID = item->GetId();
|
|
removeItem.iStackCount = proxy->GetCount();
|
|
removeItem.iStackRemaining = 0;
|
|
if (m_Parent->IsPlayer()) removeItem.SendToClient(m_Parent->GetSystemAddress());
|
|
|
|
proxy->RemoveFromInventory();
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
// The effect an item plays on its wearer when it is equipped or unequipped, as LWOItemComponent::SendMessage
|
|
// (0x00cdafb0) picks it from the equip location; none for the other locations
|
|
std::u16string EquipEffectType(const std::string& equipLocation, const bool equip) {
|
|
static const std::array<std::pair<std::string_view, std::u16string_view>, 6> SLOTS = { {
|
|
{ "special_l", u"left" }, { "special_r", u"right" }, { "hair", u"head" },
|
|
{ "clavicle", u"clavicle" }, { "chest", u"chest" }, { "legs", u"legs" },
|
|
} };
|
|
for (const auto& [location, slot] : SLOTS) {
|
|
if (equipLocation == location) return std::u16string(equip ? u"equip-" : u"unequip-") + std::u16string(slot);
|
|
}
|
|
return u"";
|
|
}
|
|
}
|
|
|
|
void InventoryComponent::SendEquipState(const Item& item, const bool equipped) const {
|
|
// Live sent every equip and unequip to everyone, proxies included
|
|
GameMessages::ChangeObjectWorldState worldState;
|
|
worldState.target = item.GetId();
|
|
worldState.newState = equipped ? eObjectWorldState::ATTACHED : eObjectWorldState::INVENTORY;
|
|
worldState.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
// The client plays no equip effect for noEquipAnimation items; live played no unequip effect for proxies (they
|
|
// go with their item)
|
|
const auto& info = item.GetInfo();
|
|
if (equipped ? info.noEquipAnimation : item.GetParent() != LWOOBJID_EMPTY) return;
|
|
|
|
GameMessages::PlayFXEffect effect;
|
|
effect.target = m_Parent->GetObjectID();
|
|
if (info.equipEffects != 0) {
|
|
effect.effectID = static_cast<int32_t>(info.equipEffects);
|
|
effect.effectType = equipped ? u"equip" : u"unequip";
|
|
} else {
|
|
effect.effectType = EquipEffectType(info.equipLocation, equipped);
|
|
}
|
|
if (effect.effectType.empty()) return;
|
|
effect.priority = 1.07f;
|
|
effect.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
void InventoryComponent::SendUncastEquipSkills(const Item& item) const {
|
|
// Proxies' equip skills aren't cast (FindBuffs), so there is nothing to uncast
|
|
if (item.GetParent() != LWOOBJID_EMPTY || !m_Parent->IsPlayer()) return;
|
|
for (const auto& skill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(item.GetLot())) {
|
|
if (skill.castOnType != 1) continue;
|
|
GameMessages::UncastSkill uncast;
|
|
uncast.target = m_Parent->GetObjectID();
|
|
uncast.skillID = static_cast<int32_t>(skill.skillID);
|
|
uncast.SendToClient(m_Parent->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
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<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;
|
|
}
|
|
|
|
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<uint32_t>(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<eItemType>(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<MissionComponent>();
|
|
|
|
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<eInventoryType>(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<eInventoryType>(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<MissionComponent*>(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<int64_t> ParseDeletionRestrictionIds(const std::string& ids) {
|
|
std::vector<int64_t> 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<int64_t>(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<int32_t>(item.GetInfo().delResIndex);
|
|
if (row < 0) return true;
|
|
|
|
std::set<int32_t> visited;
|
|
return CheckDeletionRestriction(item.GetId(), row, visited);
|
|
}
|
|
|
|
bool InventoryComponent::CheckDeletionRestriction(const LWOOBJID itemId, const int32_t row, std::set<int32_t>& 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<CDDeletionRestrictionsTable>()->Get(row);
|
|
if (!restriction || !restriction->restricted) return true;
|
|
|
|
const auto checkType = static_cast<eDeletionRestrictionCheckType>(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<int32_t>(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<int64_t>(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<int64_t> 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;
|
|
}
|
|
}
|