#include "inventorystore.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/weapontype.hpp" #include "class.hpp" #include "esmstore.hpp" void MWWorld::InventoryStore::copySlots(const InventoryStore& store) { // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds for (std::vector::const_iterator iter(const_cast(store).mSlots.begin()); iter != const_cast(store).mSlots.end(); ++iter) { std::size_t distance = std::distance(const_cast(store).begin(), *iter); ContainerStoreIterator slot = begin(); std::advance(slot, distance); mSlots.push_back(slot); } // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds std::size_t distance = std::distance( const_cast(store).begin(), const_cast(store).mSelectedEnchantItem); ContainerStoreIterator slot = begin(); std::advance(slot, distance); mSelectedEnchantItem = slot; } void MWWorld::InventoryStore::initSlots(TSlots& slots_) { for (int i = 0; i < Slots; ++i) slots_.push_back(end()); } void MWWorld::InventoryStore::storeEquipmentState( const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const { for (int i = 0; i < static_cast(mSlots.size()); ++i) if (mSlots[i].getType() != -1 && mSlots[i]->getBase() == &ref) { inventory.mEquipmentSlots[index] = i; } if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref) inventory.mSelectedEnchantItem = index; } void MWWorld::InventoryStore::readEquipmentState( const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; std::map::const_iterator found = inventory.mEquipmentSlots.find(index); if (found != inventory.mEquipmentSlots.end()) { if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) throw std::runtime_error("Invalid slot index in inventory state"); // make sure the item can actually be equipped in this slot int slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; if (std::find(allowedSlots.first.begin(), allowedSlots.first.end(), slot) == allowedSlots.first.end()) slot = allowedSlots.first.front(); // unstack if required if (!allowedSlots.second && iter->getRefData().getCount() > 1) { int count = iter->getRefData().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); iter->getRefData().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else mSlots[slot] = iter; } } MWWorld::InventoryStore::InventoryStore() : ContainerStore() , mInventoryListener(nullptr) , mUpdatesEnabled(true) , mFirstAutoEquip(true) , mSelectedEnchantItem(end()) { initSlots(mSlots); } MWWorld::InventoryStore::InventoryStore(const InventoryStore& store) : ContainerStore(store) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) , mSelectedEnchantItem(end()) { copySlots(store); } MWWorld::InventoryStore& MWWorld::InventoryStore::operator=(const InventoryStore& store) { if (this == &store) return *this; mListener = store.mListener; mInventoryListener = store.mInventoryListener; mFirstAutoEquip = store.mFirstAutoEquip; mRechargingItemsUpToDate = false; ContainerStore::operator=(store); mSlots.clear(); copySlots(store); return *this; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add( const Ptr& itemPtr, int count, bool allowAutoEquip, bool resolve) { const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, allowAutoEquip, resolve); // Auto-equip items if an armor/clothing item is added, but not for the player nor werewolves if (allowAutoEquip && mActor != MWMechanics::getPlayer() && mActor.getClass().isNpc() && !mActor.getClass().getNpcStats(mActor).isWerewolf()) { auto type = itemPtr.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(); } if (mListener) mListener->itemAdded(*retVal, count); return retVal; } void MWWorld::InventoryStore::equip(int slot, const ContainerStoreIterator& iterator) { if (iterator == end()) throw std::runtime_error("can't equip end() iterator, use unequip function instead"); if (slot < 0 || slot >= static_cast(mSlots.size())) throw std::runtime_error("slot number out of range"); if (iterator.getContainerStore() != this) throw std::runtime_error("attempt to equip an item that is not in the inventory"); std::pair, bool> slots_; slots_ = iterator->getClass().getEquipmentSlots(*iterator); if (std::find(slots_.first.begin(), slots_.first.end(), slot) == slots_.first.end()) throw std::runtime_error("invalid slot"); if (mSlots[slot] != end()) unequipSlot(slot); // unstack item pointed to by iterator if required if (iterator != end() && !slots_.second && iterator->getRefData().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { unstack(*iterator); } mSlots[slot] = iterator; flagAsModified(); fireEquipmentChangedEvent(); } void MWWorld::InventoryStore::unequipAll() { mUpdatesEnabled = false; for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) unequipSlot(slot); mUpdatesEnabled = true; fireEquipmentChangedEvent(); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) { return findSlot(slot); } MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) const { return findSlot(slot); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot(int slot) const { if (slot < 0 || slot >= static_cast(mSlots.size())) throw std::runtime_error("slot number out of range"); if (mSlots[slot] == end()) return mSlots[slot]; if (mSlots[slot]->getRefData().getCount() < 1) { // Object has been deleted // This should no longer happen, since the new remove function will unequip first throw std::runtime_error( "Invalid slot, make sure you are not calling RefData::setCount for a container object"); } return mSlots[slot]; } void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) { if (!mActor.getClass().isNpc()) { // In original game creatures do not autoequip weapon, but we need it for weapon sheathing. // The only case when the difference is noticable - when this creature sells weapon. // So just disable weapon autoequipping for creatures which sells weapon. int services = mActor.getClass().getServices(mActor); bool sellsWeapon = services & (ESM::NPC::Weapon | ESM::NPC::MagicItems); if (sellsWeapon) return; } static const ESM::RefId weaponSkills[] = { ESM::Skill::LongBlade, ESM::Skill::Axe, ESM::Skill::Spear, ESM::Skill::ShortBlade, ESM::Skill::Marksman, ESM::Skill::BluntWeapon, }; const size_t weaponSkillsLength = sizeof(weaponSkills) / sizeof(weaponSkills[0]); bool weaponSkillVisited[weaponSkillsLength] = { false }; // give arrows/bolt with max damage by default int arrowMax = 0; int boltMax = 0; ContainerStoreIterator arrow(end()); ContainerStoreIterator bolt(end()); // rate ammo for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (esmWeapon->mData.mType == ESM::Weapon::Arrow) { if (esmWeapon->mData.mChop[1] >= arrowMax) { arrowMax = esmWeapon->mData.mChop[1]; arrow = iter; } } else if (esmWeapon->mData.mType == ESM::Weapon::Bolt) { if (esmWeapon->mData.mChop[1] >= boltMax) { boltMax = esmWeapon->mData.mChop[1]; bolt = iter; } } } // rate weapon for (int i = 0; i < static_cast(weaponSkillsLength); ++i) { float max = 0; int maxWeaponSkill = -1; for (int j = 0; j < static_cast(weaponSkillsLength); ++j) { float skillValue = mActor.getClass().getSkill(mActor, weaponSkills[j]); if (skillValue > max && !weaponSkillVisited[j]) { max = skillValue; maxWeaponSkill = j; } } if (maxWeaponSkill == -1) break; max = 0; ContainerStoreIterator weapon(end()); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (MWMechanics::getWeaponType(esmWeapon->mData.mType)->mWeaponClass == ESM::WeaponType::Ammo) continue; if (iter->getClass().getEquipmentSkill(*iter) == weaponSkills[maxWeaponSkill]) { if (esmWeapon->mData.mChop[1] >= max) { max = esmWeapon->mData.mChop[1]; weapon = iter; } if (esmWeapon->mData.mSlash[1] >= max) { max = esmWeapon->mData.mSlash[1]; weapon = iter; } if (esmWeapon->mData.mThrust[1] >= max) { max = esmWeapon->mData.mThrust[1]; weapon = iter; } } } if (weapon != end() && weapon->getClass().canBeEquipped(*weapon, mActor).first) { // Do not equip ranged weapons, if there is no suitable ammo bool hasAmmo = true; const MWWorld::LiveCellRef* ref = weapon->get(); int type = ref->mBase->mData.mType; int ammotype = MWMechanics::getWeaponType(type)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) { if (arrow == end()) hasAmmo = false; else slots_[Slot_Ammunition] = arrow; } else if (ammotype == ESM::Weapon::Bolt) { if (bolt == end()) hasAmmo = false; else slots_[Slot_Ammunition] = bolt; } if (hasAmmo) { std::pair, bool> itemsSlots = weapon->getClass().getEquipmentSlots(*weapon); if (!itemsSlots.first.empty()) { if (!itemsSlots.second) { if (weapon->getRefData().getCount() > 1) { unstack(*weapon); } } int slot = itemsSlots.first.front(); slots_[slot] = weapon; if (ammotype == ESM::Weapon::None) slots_[Slot_Ammunition] = end(); } break; } } weaponSkillVisited[maxWeaponSkill] = true; } } void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { // Only NPCs can wear armor for now. // For creatures we equip only shields. if (!mActor.getClass().isNpc()) { autoEquipShield(slots_); return; } const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); float unarmoredSkill = mActor.getClass().getSkill(mActor, ESM::Skill::Unarmored); float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); ++iter) { Ptr test = *iter; switch (test.getClass().canBeEquipped(test, mActor).first) { case 0: continue; default: break; } if (iter.getType() == ContainerStore::Type_Armor && test.getClass().getEffectiveArmorRating(test, mActor) <= std::max(unarmoredRating, 0.f)) { continue; } std::pair, bool> itemsSlots = iter->getClass().getEquipmentSlots(*iter); // checking if current item pointed by iter can be equipped for (int slot : itemsSlots.first) { // if true then it means slot is equipped already // check if slot may require swapping if current item is more valuable if (slots_.at(slot) != end()) { Ptr old = *slots_.at(slot); if (iter.getType() == ContainerStore::Type_Armor) { if (old.getType() == ESM::Armor::sRecordId) { if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) continue; if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) { if (old.getClass().getEffectiveArmorRating(old, mActor) >= test.getClass().getEffectiveArmorRating(test, mActor)) // old armor had better armor rating continue; } } // suitable armor should replace already equipped clothing } else if (iter.getType() == ContainerStore::Type_Clothing) { // if left ring is equipped if (slot == Slot_LeftRing) { // if there is a place for right ring dont swap it if (slots_.at(Slot_RightRing) == end()) { continue; } else // if right ring is equipped too { Ptr rightRing = *slots_.at(Slot_RightRing); // we want to swap cheaper ring only if both are equipped if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing)) continue; } } if (old.getType() == ESM::Clothing::sRecordId) { // check value if (old.getClass().getValue(old) >= test.getClass().getValue(test)) // old clothing was more valuable continue; } else // suitable clothing should NOT replace already equipped armor continue; } } if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required if (iter->getRefData().getCount() > 1) { unstack(*iter); } } // if we are here it means item can be equipped or swapped slots_[slot] = iter; break; } } } void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_) { for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter) { if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) continue; if (iter->getClass().canBeEquipped(*iter, mActor).first != 1) continue; std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); int slot = shieldSlots.first[0]; const ContainerStoreIterator& shield = slots_[slot]; if (shield != end() && shield.getType() == Type_Armor && shield->get()->mBase->mData.mType == ESM::Armor::Shield) { if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) continue; } slots_[slot] = iter; } } void MWWorld::InventoryStore::autoEquip() { TSlots slots_; initSlots(slots_); // Disable model update during auto-equip mUpdatesEnabled = false; // Autoequip clothing, armor and weapons. // Equipping lights is handled in Actors::updateEquippedLight based on environment light. // Note: creatures ignore equipment armor rating and only equip shields // Use custom logic for them - select shield based on its health instead of armor rating autoEquipWeapon(slots_); autoEquipArmor(slots_); bool changed = false; for (std::size_t i = 0; i < slots_.size(); ++i) { if (slots_[i] != mSlots[i]) { changed = true; break; } } mUpdatesEnabled = true; if (changed) { mSlots.swap(slots_); fireEquipmentChangedEvent(); flagAsModified(); } } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield() { TSlots slots; initSlots(slots); autoEquipArmor(slots); return slots[Slot_CarriedLeft]; } bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); if (!canStack) return false; // don't stack if either item is currently equipped for (TSlots::const_iterator iter(mSlots.begin()); iter != mSlots.end(); ++iter) { if (*iter != end() && (ptr1 == **iter || ptr2 == **iter)) { bool stackWhenEquipped = (*iter)->getClass().getEquipmentSlots(**iter).second; if (!stackWhenEquipped) return false; } } return true; } void MWWorld::InventoryStore::setSelectedEnchantItem(const ContainerStoreIterator& iterator) { mSelectedEnchantItem = iterator; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSelectedEnchantItem() { return mSelectedEnchantItem; } int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplacement, bool resolve) { int retCount = ContainerStore::remove(item, count, equipReplacement, resolve); bool wasEquipped = false; if (!item.getRefData().getCount()) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { if (mSlots[slot] == end()) continue; if (*mSlots[slot] == item) { unequipSlot(slot); wasEquipped = true; break; } } } // If an armor/clothing item is removed, try to find a replacement, // but not for the player nor werewolves, and not if the RemoveItem script command // was used (equipReplacement is false) if (equipReplacement && wasEquipped && (mActor != MWMechanics::getPlayer()) && mActor.getClass().isNpc() && !mActor.getClass().getNpcStats(mActor).isWerewolf()) { auto type = item.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(); } if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } if (mListener) mListener->itemRemoved(item, retCount); return retCount; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, bool applyUpdates) { if (slot < 0 || slot >= static_cast(mSlots.size())) throw std::runtime_error("slot number out of range"); ContainerStoreIterator it = mSlots[slot]; if (it != end()) { ContainerStoreIterator retval = it; // empty this slot mSlots[slot] = end(); if (it->getRefData().getCount()) { retval = restack(*it); if (mActor == MWMechanics::getPlayer()) { // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared const ESM::RefId& script = it->getClass().getScript(*it); if (!script.empty()) (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) { mSelectedEnchantItem = end(); } } if (applyUpdates) { fireEquipmentChangedEvent(); } return retval; } return it; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItem(const MWWorld::Ptr& item) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ContainerStoreIterator equipped = getSlot(slot); if (equipped != end() && *equipped == item) return unequipSlot(slot); } throw std::runtime_error("attempt to unequip an item that is not currently equipped"); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(const Ptr& item, int count) { if (!isEquipped(item)) throw std::runtime_error("attempt to unequip an item that is not currently equipped"); if (count <= 0) throw std::runtime_error("attempt to unequip nothing (count <= 0)"); if (count > item.getRefData().getCount()) throw std::runtime_error("attempt to unequip more items than equipped"); if (count == item.getRefData().getCount()) return unequipItem(item); // Move items to an existing stack if possible, otherwise split count items out into a new stack. // Moving counts manually here, since ContainerStore's restack can't target unequipped stacks. for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (stacks(*iter, item) && !isEquipped(*iter)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); item.getRefData().setCount(subtractItems(item.getRefData().getCount(false), count)); return iter; } } return unstack(item, item.getRefData().getCount() - count); } MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } void MWWorld::InventoryStore::setInvListener(InventoryStoreListener* listener) { mInventoryListener = listener; } void MWWorld::InventoryStore::fireEquipmentChangedEvent() { if (!mUpdatesEnabled) return; if (mInventoryListener) mInventoryListener->equipmentChanged(); // if player, update inventory window /* if (mActor == MWMechanics::getPlayer()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); } */ } void MWWorld::InventoryStore::clear() { mSlots.clear(); initSlots(mSlots); ContainerStore::clear(); } bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr& item) { for (int i = 0; i < MWWorld::InventoryStore::Slots; ++i) { if (getSlot(i) != end() && *getSlot(i) == item) return true; } return false; } bool MWWorld::InventoryStore::isFirstEquip() { bool first = mFirstAutoEquip; mFirstAutoEquip = false; return first; }