/* * The Mana Server * Copyright (C) 2004-2010 The Mana World Development Team * * This file is part of The Mana Server. * * The Mana Server is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * any later version. * * The Mana Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with The Mana Server. If not, see . */ #include #include #include "game-server/gamehandler.h" #include "game-server/inventory.h" #include "game-server/item.h" #include "game-server/itemmanager.h" #include "game-server/state.h" #include "net/messageout.h" #include "utils/logger.h" Inventory::Inventory(Entity *p): mPoss(&p->getComponent()->getPossessions()), mCharacter(p) { } Inventory::Inventory(Entity *p, Possessions &possessions): mPoss(&possessions), mCharacter(p) { } void Inventory::sendFull() const { /* Sends all the information needed to construct inventory and equipment to the client */ MessageOut m(GPMSG_INVENTORY_FULL); m.writeInt16(mPoss->inventory.size()); for (InventoryData::const_iterator l = mPoss->inventory.begin(), l_end = mPoss->inventory.end(); l != l_end; ++l) { assert(l->second.itemId); m.writeInt16(l->first); // Slot id m.writeInt16(l->second.itemId); m.writeInt16(l->second.amount); } for (EquipData::const_iterator k = mPoss->equipSlots.begin(), k_end = mPoss->equipSlots.end(); k != k_end; ++k) { m.writeInt16(k->first); // Equip slot id m.writeInt16(k->second.itemId); // Item id m.writeInt16(k->second.itemInstance); // Item instance } gameHandler->sendTo(mCharacter, m); } void Inventory::initialize() { /* * Construct a set of item Ids to keep track of duplicate item Ids. */ std::set itemIds; /* * Construct a set of itemIds to keep track of duplicate itemIds. */ InventoryData::iterator it1; for (it1 = mPoss->inventory.begin(); it1 != mPoss->inventory.end();) { ItemClass *item = itemManager->getItem(it1->second.itemId); if (item) { // If the insertion succeeded, it's the first time we're // adding the item in the inventory. Hence, we can trigger // item presence in inventory effect. if (itemIds.insert(it1->second.itemId).second) item->useTrigger(mCharacter, ITT_IN_INVY); ++it1; } else { LOG_WARN("Inventory: deleting unknown item type " << it1->second.itemId << " from the inventory of '" << mCharacter->getComponent()->getName() << "'!"); mPoss->inventory.erase(it1++); } } itemIds.clear(); /* * Equipment effects can be cumulative if more than one item instance * is equipped, but we check to trigger the item presence in equipment * effect only based on the first item instance insertion. */ EquipData::iterator it2; for (it2 = mPoss->equipSlots.begin(); it2 != mPoss->equipSlots.end();) { ItemClass *item = itemManager->getItem(it2->second.itemId); if (item) { // TODO: Check equip conditions. // If not all needed slots are there, put the item back // in the inventory. } else { LOG_WARN("Equipment: deleting unknown item id " << it2->second.itemId << " from the equipment of '" << mCharacter->getComponent()->getName() << "'!"); mPoss->equipSlots.erase(it2++); continue; } /* * Apply all equip triggers at first item instance insertion */ if (itemIds.insert(it2->second.itemInstance).second) { itemManager->getItem(it2->second.itemId) ->useTrigger(mCharacter, ITT_EQUIP); } ++it2; } } unsigned Inventory::getItem(unsigned slot) const { InventoryData::iterator item = mPoss->inventory.find(slot); return item != mPoss->inventory.end() ? item->second.itemId : 0; } unsigned Inventory::insert(unsigned itemId, unsigned amount) { if (!itemId || !amount) return 0; MessageOut invMsg(GPMSG_INVENTORY); ItemClass *item = itemManager->getItem(itemId); if (!item) { LOG_ERROR("Inventory: Trying to insert invalid item id " << itemId << " (amount: " << amount << ")"); return amount; } unsigned maxPerSlot = item->getMaxPerSlot(); LOG_DEBUG("Inventory: Inserting " << amount << " item(s) Id: " << itemId << " for character '" << mCharacter->getComponent()->getName() << "'."); InventoryData::iterator it, it_end = mPoss->inventory.end(); // Add to slots with existing items of this type first. for (it = mPoss->inventory.begin(); it != it_end; ++it) { if (it->second.itemId == itemId) { // If the slot is full, try the next slot if (it->second.amount >= maxPerSlot) continue; // Add everything that'll fit to the stack unsigned short spaceLeft = maxPerSlot - it->second.amount; if (spaceLeft >= amount) { it->second.amount += amount; amount = 0; LOG_DEBUG("Everything inserted at slot id: " << it->first); } else { it->second.amount += spaceLeft; amount -= spaceLeft; LOG_DEBUG(spaceLeft << " item(s) inserted at slot id: " << it->first); } invMsg.writeInt16(it->first); invMsg.writeInt16(itemId); invMsg.writeInt16(it->second.amount); if (!amount) break; } } int slot = 0; // We still have some left, so add to blank slots. for (it = mPoss->inventory.begin();; ++it) { if (!amount) break; int lim = (it == it_end) ? INVENTORY_SLOTS : it->first; while (amount && slot < lim) { int additions = std::min(amount, maxPerSlot); mPoss->inventory[slot].itemId = itemId; mPoss->inventory[slot].amount = additions; amount -= additions; LOG_DEBUG(additions << " item(s) inserted at slot id: " << slot); invMsg.writeInt16(slot++); // Last read, so also increment invMsg.writeInt16(itemId); invMsg.writeInt16(additions); } ++slot; // Skip the slot that the iterator points to if (it == it_end) break; } item->useTrigger(mCharacter, ITT_IN_INVY); // Send that first, before checking potential removals if (invMsg.getLength() > 2) gameHandler->sendTo(mCharacter, invMsg); return amount; } unsigned Inventory::count(unsigned itemId, bool inInventory, bool inEquipment) const { unsigned nb = 0; if (inInventory) { for (InventoryData::iterator it = mPoss->inventory.begin(), it_end = mPoss->inventory.end(); it != it_end; ++it) if (it->second.itemId == itemId) nb += it->second.amount; } if (inEquipment) { std::set itemInstances; for (EquipData::iterator it = mPoss->equipSlots.begin(), it_end = mPoss->equipSlots.end(); it != it_end; ++it) { if (it->second.itemId != itemId || !it->second.itemInstance) continue; // If the insertion was successful, then it was the first time, // and can be counted. if ((itemInstances.insert(it->second.itemInstance)).second) ++nb; } } return nb; } int Inventory::getFirstSlot(unsigned itemId) { for (InventoryData::iterator it = mPoss->inventory.begin(), it_end = mPoss->inventory.end(); it != it_end; ++it) if (it->second.itemId == itemId) return (int)it->first; return -1; } unsigned Inventory::remove(unsigned itemId, unsigned amount) { if (!itemId || !amount) return amount; LOG_DEBUG("Inventory: Request remove of " << amount << " item(s) id: " << itemId << " for character: '" << mCharacter->getComponent()->getName() << "'."); MessageOut invMsg(GPMSG_INVENTORY); bool triggerLeaveInventory = true; for (InventoryData::iterator it = mPoss->inventory.begin(); it != mPoss->inventory.end();) { LOG_DEBUG("Remove: Treating slot id: " << it->first); if (it->second.itemId == itemId) { if (amount) { unsigned sub = std::min(amount, it->second.amount); amount -= sub; it->second.amount -= sub; invMsg.writeInt16(it->first); if (it->second.amount) { invMsg.writeInt16(it->second.itemId); invMsg.writeInt16(it->second.amount); // Some still exist, and we have none left to remove, so // no need to run leave invy triggers. if (!amount) triggerLeaveInventory = false; LOG_DEBUG("Slot id: " << it->first << " has now " << it->second.amount << "item(s)."); } else { invMsg.writeInt16(0); // Ensure the slot is set empty. LOG_DEBUG("Slot id: " << it->first << " is now empty."); mPoss->inventory.erase(it++); continue; } } else { // We found an instance of them existing and have none left to // remove, so no need to run leave invy triggers. triggerLeaveInventory = false; } } ++it; } if (triggerLeaveInventory) itemManager->getItem(itemId)->useTrigger(mCharacter, ITT_LEAVE_INVY); if (invMsg.getLength() > 2) gameHandler->sendTo(mCharacter, invMsg); return amount; } unsigned Inventory::move(unsigned slot1, unsigned slot2, unsigned amount) { LOG_DEBUG(amount << " item(s) requested to move from: " << slot1 << " to " << slot2 << " for character: '" << mCharacter->getComponent()->getName() << "'."); if (!amount || slot1 == slot2 || slot2 >= INVENTORY_SLOTS) return amount; InventoryData::iterator it1 = mPoss->inventory.find(slot1), it2 = mPoss->inventory.find(slot2), inv_end = mPoss->inventory.end(); if (it1 == inv_end) return amount; MessageOut invMsg(GPMSG_INVENTORY); unsigned nb = std::min(amount, it1->second.amount); if (it2 == inv_end) { // Slot2 does not yet exist. mPoss->inventory[slot2].itemId = it1->second.itemId; nb = std::min(itemManager->getItem(it1->second.itemId)->getMaxPerSlot(), nb); mPoss->inventory[slot2].amount = nb; it1->second.amount -= nb; amount -= nb; //Save the itemId in case of deletion of the iterator unsigned itemId = it1->second.itemId; invMsg.writeInt16(slot1); // Slot if (it1->second.amount) { invMsg.writeInt16(it1->second.itemId); // Item Id invMsg.writeInt16(it1->second.amount); // Amount LOG_DEBUG("Left " << amount << " item(s) id:" << it1->second.itemId << " into slot: " << slot1); } else { invMsg.writeInt16(0); mPoss->inventory.erase(it1); LOG_DEBUG("Slot: " << slot1 << " is now empty."); } invMsg.writeInt16(slot2); // Slot invMsg.writeInt16(itemId); // Item Id (same as slot 1) invMsg.writeInt16(nb); // Amount LOG_DEBUG("Slot: " << slot2 << " has now " << nb << " of item id: " << itemId); } else { // Slot2 exists. if (it2->second.itemId != it1->second.itemId) { // Swap items when they are of a different type // and when all the amount of slot 1 is moving onto slot 2. if (amount >= it1->second.amount) { unsigned itemId = it1->second.itemId; unsigned amount = it1->second.amount; it1->second.itemId = it2->second.itemId; it1->second.amount = it2->second.amount; it2->second.itemId = itemId; it2->second.amount = amount; // Sending swapped slots. invMsg.writeInt16(slot1); invMsg.writeInt16(it1->second.itemId); invMsg.writeInt16(it1->second.amount); invMsg.writeInt16(slot2); invMsg.writeInt16(it2->second.itemId); invMsg.writeInt16(it2->second.amount); LOG_DEBUG("Swapping items in slots " << slot1 << " and " << slot2); } else { // Cannot partially stack items of a different type. LOG_DEBUG("Cannot move " << amount << " item(s) from slot " << slot1 << " to " << slot2); return amount; } } else // Same item type on slot 2. { // Number of items moving nb = std::min(itemManager->getItem( it1->second.itemId)->getMaxPerSlot() - it2->second.amount, nb); // If nothing can move, we can abort if (!nb) return amount; it1->second.amount -= nb; it2->second.amount += nb; amount -= nb; invMsg.writeInt16(slot1); // Slot if (it1->second.amount) { invMsg.writeInt16(it1->second.itemId); // Item Id invMsg.writeInt16(it1->second.amount); // Amount } else { invMsg.writeInt16(0); mPoss->inventory.erase(it1); } invMsg.writeInt16(slot2); // Slot invMsg.writeInt16(it2->second.itemId); // Item Id invMsg.writeInt16(it2->second.amount); // Amount } } if (invMsg.getLength() > 2) gameHandler->sendTo(mCharacter, invMsg); return amount; } unsigned Inventory::removeFromSlot(unsigned slot, unsigned amount) { InventoryData::iterator it = mPoss->inventory.find(slot); // When the given slot doesn't exist, we can't remove anything if (it == mPoss->inventory.end()) return amount; LOG_DEBUG("Inventory: Request Removal of " << amount << " item(s) in slot: " << slot << " for character: '" << mCharacter->getComponent()->getName() << "'."); MessageOut invMsg(GPMSG_INVENTORY); // Check if an item of the same id exists elsewhere in the inventory bool exists = false; for (InventoryData::const_iterator it2 = mPoss->inventory.begin(), it2_end = mPoss->inventory.end(); it2 != it2_end; ++it2) { if (it2->second.itemId == it->second.itemId && it->first != it2->first) { exists = true; break; } } // We check whether it's the last slot where we can find that item id. bool lastSlotOfItemRemaining = false; if (!exists && it->second.itemId) lastSlotOfItemRemaining = true; unsigned sub = std::min(amount, it->second.amount); amount -= sub; it->second.amount -= sub; invMsg.writeInt16(it->first); if (it->second.amount) { invMsg.writeInt16(it->second.itemId); invMsg.writeInt16(it->second.amount); } else { invMsg.writeInt16(0); // The item(s) was(were) the last one(s) in the inventory. if (lastSlotOfItemRemaining) { if (ItemClass *ic = itemManager->getItem(it->second.itemId)) ic->useTrigger(mCharacter, ITT_LEAVE_INVY); } mPoss->inventory.erase(it); } if (invMsg.getLength() > 2) gameHandler->sendTo(mCharacter, invMsg); return amount; } void Inventory::updateEquipmentTrigger(unsigned oldId, unsigned newId) { if (!oldId && !newId) return; updateEquipmentTrigger(oldId ? itemManager->getItem(oldId) : 0, newId ? itemManager->getItem(newId) : 0); } void Inventory::updateEquipmentTrigger(ItemClass *oldI, ItemClass *newI) { // This should only be called when applying changes, either directly // in non-delayed mode or when the changes are committed in delayed mode. if (!oldI && !newI) return; if (oldI && newI) oldI->useTrigger(mCharacter, ITT_EQUIPCHG); else if (oldI) oldI->useTrigger(mCharacter, ITT_UNEQUIP); else if (newI) newI->useTrigger(mCharacter, ITT_EQUIP); } unsigned Inventory::getNewEquipItemInstance() { std::set alreadyUsed; for (EquipData::const_iterator it = mPoss->equipSlots.begin(); it != mPoss->equipSlots.end(); ++it) { alreadyUsed.insert(it->second.itemInstance); } unsigned itemInstance = 1; while (alreadyUsed.count(itemInstance)) itemInstance++; return itemInstance; } bool Inventory::checkEquipmentCapacity(unsigned equipmentSlot, unsigned capacityRequested) { int capacity = itemManager->getEquipSlotCapacity(equipmentSlot); // If the equipement slot doesn't exist, we can't equip on it. if (capacity <= 0) return false; // Test whether the slot capacity requested is reached. for (EquipData::const_iterator it = mPoss->equipSlots.begin(), it_end = mPoss->equipSlots.end(); it != it_end; ++it) { if (it->first == equipmentSlot) { if (it->second.itemInstance != 0) { capacity--; } } } assert(capacity >= 0); // A should never happen case. if (capacity < (int)capacityRequested) return false; return true; } bool Inventory::equip(int inventorySlot) { // Test inventory slot existence InventoryData::iterator it; if ((it = mPoss->inventory.find(inventorySlot)) == mPoss->inventory.end()) { LOG_DEBUG("No existing item in inventory at slot: " << inventorySlot); return false; } // Test the equipment scripted requirements if (!testEquipScriptRequirements(it->second.itemId)) return false; // Test the equip requirements. If none, it's not an equipable item. const ItemEquipRequirement &equipReq = itemManager->getItem(it->second.itemId)->getItemEquipRequirement(); if (!equipReq.equipSlotId) { LOG_DEBUG("No equip requirements for item id: " << it->second.itemId << " at slot: " << inventorySlot); return false; } // List of potential unique itemInstances to unequip first. std::set equipInstancesToUnequipFirst; // We first check the equipment slots for: // - 1. whether enough total equip slot space is available. // - 2. whether some other equipment is to be unequipped first. // If not enough total space in the equipment slot is available, // we cannot equip. if (itemManager->getEquipSlotCapacity(equipReq.equipSlotId) < equipReq.capacityRequired) { LOG_DEBUG("Not enough equip capacity at slot: " << equipReq.equipSlotId << ", total available: " << itemManager->getEquipSlotCapacity(equipReq.equipSlotId) << ", required: " << equipReq.capacityRequired); return false; } // Test whether some item(s) is(are) to be unequipped first. if (!checkEquipmentCapacity(equipReq.equipSlotId, equipReq.capacityRequired)) { // And test whether the unequip action would succeed first. if (testUnequipScriptRequirements(equipReq.equipSlotId) && hasInventoryEnoughSpace(equipReq.equipSlotId)) { // Then, we unequip each iteminstance of the equip slot for (EquipData::iterator iter = mPoss->equipSlots.begin(); iter != mPoss->equipSlots.end(); ++iter) { if (iter->first == equipReq.equipSlotId && iter->second.itemInstance) equipInstancesToUnequipFirst.insert( iter->second.itemInstance); } } else { // Some non-unequippable equipment is to be unequipped first. // Can be the case of cursed items, // or when the inventory is full, for instance. return false; } } // Potential Pre-unequipment process for (std::set::const_iterator it3 = equipInstancesToUnequipFirst.begin(); it3 != equipInstancesToUnequipFirst.end(); ++it3) { if (!unequip(*it3)) { // Something went wrong even when we tested the unequipment process. LOG_WARN("Unable to unequip even when unequip was tested. " "Character : " << mCharacter->getComponent()->getName() << ", unequip slot: " << *it3); return false; } } // Actually equip the item now that the requirements has met. //W equip slot type count, W item id, { W equip slot, W capacity used}* MessageOut equipMsg(GPMSG_EQUIP); equipMsg.writeInt16(it->second.itemId); // Item Id equipMsg.writeInt16(1); // Number of equip slot changed. // Compute an unique equip item Instance id (unicity is per character only.) int itemInstance = getNewEquipItemInstance(); unsigned capacityLeft = equipReq.capacityRequired; unsigned capacityUsed = 0; // Apply equipment changes for (EquipData::iterator it4 = mPoss->equipSlots.begin(), it4_end = mPoss->equipSlots.end(); it4 != it4_end; ++it4) { if (!capacityLeft) break; // We've found an existing equip slot if (it4->first == equipReq.equipSlotId) { // We've found an empty slot if (it4->second.itemInstance == 0) { it4->second.itemId = it->second.itemId; it4->second.itemInstance = itemInstance; --capacityLeft; } else // The slot is already in use. { ++capacityUsed; } } } // When there is still something to apply even when out of that loop, // It means that the equip multimapis missing empty slots. // Hence, we add them back if(capacityLeft) { unsigned maxCapacity = itemManager->getEquipSlotCapacity(equipReq.equipSlotId); // A should never happen case assert(maxCapacity >= capacityUsed + capacityLeft); while (capacityLeft) { EquipmentItem equipItem(it->second.itemId, itemInstance); mPoss->equipSlots.insert( std::make_pair(equipReq.equipSlotId, equipItem)); --capacityLeft; } } // Equip slot equipMsg.writeInt16(equipReq.equipSlotId); // Capacity used equipMsg.writeInt16(equipReq.capacityRequired); // Item instance equipMsg.writeInt16(itemInstance); // New item trigger updateEquipmentTrigger(0, it->second.itemId); // Remove item from inventory removeFromSlot(inventorySlot, 1); gameHandler->sendTo(mCharacter, equipMsg); // Update look when necessary checkLookchanges(equipReq.equipSlotId); return true; } unsigned Inventory::getSlotItemInstance(unsigned slot) { EquipData::iterator it = mPoss->equipSlots.find(slot); if (it != mPoss->equipSlots.end()) return it->second.itemInstance; return 0; } bool Inventory::unequipItem(unsigned itemId) { std::set itemInstances; for (EquipData::iterator it = mPoss->equipSlots.begin(), it_end = mPoss->equipSlots.end(); it != it_end; ++it) { if (it->second.itemId == itemId) itemInstances.insert(it->second.itemInstance); } // Nothing to do but it's a success if (itemInstances.empty()) return true; for (std::set::const_iterator it = itemInstances.begin(), it_end = itemInstances.end(); it != it_end; ++it) { if (!unequip(*it)) return false; } return true; } bool Inventory::unequip(unsigned itemInstance) { if (!itemInstance) return false; // The itemId to unequip unsigned itemId = 0; unsigned slotTypeId = 0; bool addedToInventory = false; // Empties all equip entries that point to the given equipment slot // The equipment slots should NEVER be erased after initialization! for (EquipData::iterator it = mPoss->equipSlots.begin(), it_end = mPoss->equipSlots.end(); it != it_end; ++it) { if (it->second.itemInstance == itemInstance && it->second.itemId) { // Add the item to the inventory list if not already present there itemId = it->second.itemId; // Move the item back to inventory and return false when it failed. if (!addedToInventory && insert(itemId, 1) > 0) return false; else addedToInventory = true; it->second.itemId = 0; it->second.itemInstance = 0; // We keep track of the slot type to be able to raise a potential // change in the character sprite slotTypeId = it->first; } } // When there were no corresponding item id, it means no item was to // be unequipped. if (!itemId) return false; MessageOut equipMsg(GPMSG_EQUIP); equipMsg.writeInt16(0); // Item Id, useless in case of unequip. equipMsg.writeInt16(1); // Number of slot types touched. equipMsg.writeInt16(itemInstance); equipMsg.writeInt16(0); // Capacity used, set to 0 to unequip. gameHandler->sendTo(mCharacter, equipMsg); // Apply unequip trigger updateEquipmentTrigger(itemId, 0); checkLookchanges(slotTypeId); return true; } void Inventory::checkLookchanges(unsigned slotTypeId) { if (itemManager->isEquipSlotVisible(slotTypeId)) mCharacter->getComponent()->raiseUpdateFlags( UPDATEFLAG_LOOKSCHANGE); }