From a12a99181ee4d91b0d8460075b35c8e31b8f1ccd Mon Sep 17 00:00:00 2001 From: jromap7 Date: Wed, 8 Jan 2025 13:33:44 +0100 Subject: [PATCH 1/3] Add stash support feature --- src/configmanager.cpp | 2 + src/configmanager.h | 2 + src/const.h | 8 +++ src/container.cpp | 40 +++++++++++++++ src/container.h | 2 + src/enums.h | 6 +++ src/game.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++ src/game.h | 3 ++ src/iologindata.cpp | 36 ++++++++++++++ src/item.h | 1 + src/luascript.cpp | 100 +++++++++++++++++++++++++++++++++++++ src/luascript.h | 6 +++ src/player.cpp | 80 ++++++++++++++++++++++++++++++ src/player.h | 53 ++++++++++++++++++++ src/protocolgame.cpp | 53 ++++++++++++++++++++ src/protocolgame.h | 2 + src/tools.cpp | 14 ++++++ src/tools.h | 2 + 18 files changed, 522 insertions(+) diff --git a/src/configmanager.cpp b/src/configmanager.cpp index f3ec1170f0..4884c9a2bf 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -245,6 +245,7 @@ bool ConfigManager::load() boolean[TWO_FACTOR_AUTH] = getGlobalBoolean(L, "enableTwoFactorAuth", true); boolean[CHECK_DUPLICATE_STORAGE_KEYS] = getGlobalBoolean(L, "checkDuplicateStorageKeys", false); boolean[MONSTER_OVERSPAWN] = getGlobalBoolean(L, "monsterOverspawn", false); + boolean[STASH_MOVING] = getGlobalBoolean(L, "stashMoving", false); string[DEFAULT_PRIORITY] = getGlobalString(L, "defaultPriority", "high"); string[SERVER_NAME] = getGlobalString(L, "serverName", ""); @@ -292,6 +293,7 @@ bool ConfigManager::load() integer[QUEST_TRACKER_PREMIUM_LIMIT] = getGlobalNumber(L, "questTrackerPremiumLimit", 15); integer[STAMINA_REGEN_MINUTE] = getGlobalNumber(L, "timeToRegenMinuteStamina", 3 * 60); integer[STAMINA_REGEN_PREMIUM] = getGlobalNumber(L, "timeToRegenMinutePremiumStamina", 6 * 60); + integer[STASH_ITEMS] = getGlobalNumber(L, "stashItems", 2000); expStages = loadXMLStages(); if (expStages.empty()) { diff --git a/src/configmanager.h b/src/configmanager.h index 8268afc107..d59015c172 100644 --- a/src/configmanager.h +++ b/src/configmanager.h @@ -47,6 +47,7 @@ enum boolean_config_t MANASHIELD_BREAKABLE, CHECK_DUPLICATE_STORAGE_KEYS, MONSTER_OVERSPAWN, + STASH_MOVING, LAST_BOOLEAN_CONFIG /* this must be the last one */ }; @@ -121,6 +122,7 @@ enum integer_config_t QUEST_TRACKER_PREMIUM_LIMIT, STAMINA_REGEN_MINUTE, STAMINA_REGEN_PREMIUM, + STASH_ITEMS, LAST_INTEGER_CONFIG /* this must be the last one */ }; diff --git a/src/const.h b/src/const.h index c7dc5cfc55..ffc7cfcc80 100644 --- a/src/const.h +++ b/src/const.h @@ -599,6 +599,7 @@ enum item_t : uint16_t ITEM_AMULETOFLOSS = 2173, ITEM_DOCUMENT_RO = 1968, // read-only + ITEM_SUPPLY_STASH = 31406, //read-only }; enum ResourceTypes_t : uint8_t @@ -723,6 +724,13 @@ enum CreatureIcon_t : uint8_t CREATURE_ICON_LAST = CREATURE_ICON_CROSS_RED }; +enum Supply_Stash_Actions_t : uint8_t { + SUPPLY_STASH_ACTION_STOW_ITEM = 0, + SUPPLY_STASH_ACTION_STOW_CONTAINER = 1, + SUPPLY_STASH_ACTION_STOW_STACK = 2, + SUPPLY_STASH_ACTION_WITHDRAW = 3 +}; + static constexpr int32_t CHANNEL_GUILD = 0x00; static constexpr int32_t CHANNEL_PARTY = 0x01; static constexpr int32_t CHANNEL_PRIVATE = 0xFFFF; diff --git a/src/container.cpp b/src/container.cpp index e35443e4a9..c6d7243dc7 100644 --- a/src/container.cpp +++ b/src/container.cpp @@ -98,6 +98,36 @@ void Container::addItem(Item* item) item->setParent(this); } +StashContainerList Container::getStowableItems() const { + StashContainerList toReturnList; + // List of items that cannot be stowed (coin types) + const std::set excludedItemIds = { + ITEM_GOLD_COIN, + ITEM_PLATINUM_COIN, + ITEM_CRYSTAL_COIN + }; + // Iterate through items in the container + for (auto item : itemlist) { + if (item->getContainer() != nullptr) { + // Recursively get stowable items from sub-containers + auto subContainer = item->getContainer()->getStowableItems(); + for (auto subContItem : subContainer) { + Item* containerItem = subContItem.first; + // Exclude items from the list if they are in the excluded set + if (excludedItemIds.find(containerItem->getID()) == excludedItemIds.end()) { + toReturnList.push_back(std::pair(containerItem, static_cast(containerItem->getItemCount()))); + } + } + } else if (item->isStackable()) { + // Exclude non-container stackable items if they are in the excluded set + if (excludedItemIds.find(item->getID()) == excludedItemIds.end()) { + toReturnList.push_back(std::pair(item, static_cast(item->getItemCount()))); + } + } + } + return toReturnList; +} + Attr_ReadValue Container::readAttr(AttrTypes_t attr, PropStream& propStream) { if (attr == ATTR_CONTAINER_ITEMS) { @@ -151,6 +181,16 @@ void Container::updateItemWeight(int32_t diff) } } +uint16_t Container::getFreeSlots() { + uint16_t counter = std::max(0, capacity() - size()); + for (auto item : itemlist) { + if (Container* container = item->getContainer()) { + counter += std::max(0, container->getFreeSlots()); + } + } + return counter; +} + uint32_t Container::getWeight() const { return Item::getWeight() + totalWeight; } Item* Container::getItemByIndex(size_t index) const diff --git a/src/container.h b/src/container.h index 15dbd2dad4..f0e1521f29 100644 --- a/src/container.h +++ b/src/container.h @@ -74,7 +74,9 @@ class Container : public Item, public Cylinder bool isHoldingItem(const Item* item) const; uint32_t getItemHoldingCount() const; + uint16_t getFreeSlots(); uint32_t getWeight() const override final; + StashContainerList getStowableItems() const; bool isUnlocked() const { return unlocked; } bool hasPagination() const { return pagination; } diff --git a/src/enums.h b/src/enums.h index 9c8cf1ffe8..8fcd6ef5b0 100644 --- a/src/enums.h +++ b/src/enums.h @@ -655,6 +655,12 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ShopInfoList = std::list; +struct StashItem { + uint16_t clientId; + uint32_t itemCount; +}; +using StashItemList = std::map; // itemId, StashItem + enum MonstersEvent_t : uint8_t { MONSTERS_EVENT_NONE = 0, diff --git a/src/game.cpp b/src/game.cpp index 9a871adb52..5a986598af 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2554,6 +2554,111 @@ void Game::playerBrowseField(uint32_t playerId, const Position& pos) } } +void Game::playerStowItem(uint32_t playerId, const Position &pos, uint16_t clientId, uint8_t stackpos, uint8_t count, bool allItems) { + Player* player = getPlayerByID(playerId); + if (!player) { + //std::cout << "Error: Player not found for ID: " << playerId << std::endl; + return; + } + if (!player->isPremium()) { + //std::cout << "Error: Player does not have a premium account." << std::endl; + player->sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT); + return; + } + // Get the itemId using clientId + const ItemType& it = Item::items.getItemIdByClientId(clientId); + if (it.id == 0) { + //std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; + return; + } + uint16_t itemId = it.id; // Reassign itemId from the retrieved ItemType + Thing* thing = internalGetThing(player, pos, stackpos, itemId, STACKPOS_TOPDOWN_ITEM); + if (!thing) { + //std::cout << "Error: Thing not found at position: " << pos << " with stackpos: " << (int)stackpos << " and itemId: " << itemId << std::endl; + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + Item* item = thing->getItem(); + if (!item) { + //std::cout << "Error: Thing is not an item." << std::endl; + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + if (item->getID() != itemId) { + //std::cout << "Error: Item ID mismatch. Expected: " << itemId << " but got: " << item->getID() << std::endl; + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + if (item->getItemCount() < count) { + //std::cout << "Error: Not enough items to stow. Requested count: " << (int)count << " but only have: " << item->getItemCount() << std::endl; + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + if (pos.x != 0xFFFF && !Position::areInRange<1, 1, 0>(pos, player->getPosition())) { + //std::cout << "Error: Player is not in range. Player position: " << player->getPosition() << " Item position: " << pos << std::endl; + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + //std::cout << "Success: Stowing item. Item ID: " << itemId << " Count: " << (int)count << " All items: " << allItems << std::endl; + player->stowItem(item, count, allItems); +} + +void Game::playerStashWithdraw(uint32_t playerId, uint16_t itemId, uint32_t count, uint8_t) { + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + if (player->hasFlag(PlayerFlags::PlayerFlag_CannotPickupItem)) { + return; + } + const ItemType &it = Item::items[itemId]; + if (it.id == 0 || count == 0) { + return; + } + uint16_t freeSlots = player->getFreeBackpackSlots(); + if (freeSlots == 0) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + return; + } + if (player->getFreeCapacity() < 100) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHCAPACITY); + return; + } + + // Assume stack size of 100 for stackable items, and 1 for non-stackable + uint32_t stackSize = it.stackable ? 100 : 1; + int32_t NDSlots = ((freeSlots) - (count < stackSize ? 1 : (count / stackSize))); + uint32_t SlotsWith = count; + uint32_t noSlotsWith = 0; + if (NDSlots <= 0) { + SlotsWith = (freeSlots * stackSize); + noSlotsWith = (count - SlotsWith); + } + uint32_t capWith = count; + uint32_t noCapWith = 0; + if (player->getFreeCapacity() < (count * it.weight)) { + capWith = (player->getFreeCapacity() / it.weight); + noCapWith = (count - capWith); + } + std::stringstream ss; + uint32_t WithdrawCount = (SlotsWith > capWith ? capWith : SlotsWith); + uint32_t NoWithdrawCount = (noSlotsWith < noCapWith ? noCapWith : noSlotsWith); + const char* NoWithdrawMsg = (noSlotsWith < noCapWith ? "capacity" : "slots"); + if (WithdrawCount != count) { + ss << "Retrieved " << WithdrawCount << "x " << it.name << ".\n"; + ss << NoWithdrawCount << "x are impossible to retrieve due to insufficient inventory " << NoWithdrawMsg << "."; + } else { + ss << "Retrieved " << WithdrawCount << "x " << it.name << '.'; + } + player->sendTextMessage(MESSAGE_STATUS_DEFAULT, ss.str()); + if (player->withdrawItem(itemId, WithdrawCount)) { + player->addItemFromStash(it.id, WithdrawCount); + } else { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + } + player->sendOpenStash(true); +} + void Game::playerSeekInContainer(uint32_t playerId, uint8_t containerId, uint16_t index) { Player* player = getPlayerByID(playerId); @@ -5922,3 +6027,10 @@ bool Game::reload(ReloadTypes_t reloadType) } return true; } + +bool Game::tryRetrieveStashItems(Player* player, Item* item) { + if (!player || !item) { + return RETURNVALUE_NOTPOSSIBLE; + } + return RETURNVALUE_NOERROR; +} \ No newline at end of file diff --git a/src/game.h b/src/game.h index 1ad03f1454..57506ba794 100644 --- a/src/game.h +++ b/src/game.h @@ -326,6 +326,8 @@ class Game void playerOpenChannel(uint32_t playerId, uint16_t channelId); void playerCloseChannel(uint32_t playerId, uint16_t channelId); void playerOpenPrivateChannel(uint32_t playerId, std::string receiver); + void playerStowItem(uint32_t playerId, const Position &pos, uint16_t itemId, uint8_t stackpos, uint8_t count, bool allItems); + void playerStashWithdraw(uint32_t playerId, uint16_t itemId, uint32_t count, uint8_t stackpos); void playerCloseNpcChannel(uint32_t playerId); void playerReceivePing(uint32_t playerId); void playerReceivePingBack(uint32_t playerId); @@ -491,6 +493,7 @@ class Game void addTileToClean(Tile* tile) { tilesToClean.emplace(tile); } void removeTileToClean(Tile* tile) { tilesToClean.erase(tile); } void clearTilesToClean() { tilesToClean.clear(); } + bool tryRetrieveStashItems(Player* player, Item* item); private: bool playerSaySpell(Player* player, SpeakClasses type, const std::string& text); diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 28c1936840..f27008ac99 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -486,6 +486,19 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) } } } + + // load stash items + query.str(std::string()); + query << "SELECT `item_count`, `item_id`, `client_id` FROM `player_stash` WHERE `player_id` = " << player->getGUID(); + if ((result = db.storeQuery(query.str()))) { + do { + uint16_t itemId = result->getNumber("item_id"); + uint32_t itemCount = result->getNumber("item_count"); + uint16_t clientId = result->getNumber("client_id"); + // Now add the item to the player's stash with both itemCount and clientId + player->addItemOnStash(itemId, itemCount, clientId); + } while (result->next()); + } // load storage map if ((result = db.storeQuery( @@ -742,6 +755,29 @@ bool IOLoginData::savePlayer(Player* player) if (!db.executeQuery(query.str())) { return false; } + + // Save Player Stash + query.str(std::string()); + query << "DELETE FROM `player_stash` WHERE `player_id` = " << player->getGUID(); + if (!db.executeQuery(query.str())) { + return false; + } + query.str(std::string()); + DBInsert stashQuery("INSERT INTO `player_stash` (`player_id`, `item_id`, `item_count`, `client_id`) VALUES "); + for (const auto& stashItem : player->getStashItems()) { + uint16_t itemId = stashItem.first; // Accessing the key (itemId) + uint32_t itemCount = stashItem.second.itemCount; // Accessing the item count + uint16_t clientId = stashItem.second.clientId; // Accessing the clientId + query.str(std::string()); + query << player->getGUID() << ',' << itemId << ',' << itemCount << ',' << clientId; + + if (!stashQuery.addRow(query)) { + return false; + } + } + if (!stashQuery.execute()) { + return false; + } // learned spells if (!db.executeQuery(fmt::format("DELETE FROM `player_spells` WHERE `player_id` = {:d}", player->getGUID()))) { diff --git a/src/item.h b/src/item.h index 0dafd089ec..1a61c80380 100644 --- a/src/item.h +++ b/src/item.h @@ -943,5 +943,6 @@ class Item : virtual public Thing using ItemList = std::list; using ItemDeque = std::deque; +using StashContainerList = std::vector>; #endif // FS_ITEM_H diff --git a/src/luascript.cpp b/src/luascript.cpp index c5142366ed..270cc23fee 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -2928,6 +2928,12 @@ void LuaScriptInterface::registerFunctions() registerMethod(L, "Player", "sendResourceBalance", LuaScriptInterface::luaPlayerSendResourceBalance); registerMethod(L, "Player", "sendEnterMarket", LuaScriptInterface::luaPlayerSendEnterMarket); + + registerMethod("Player", "getStashItemCount", LuaScriptInterface::luaPlayerGetStashItemCount); + registerMethod("Player", "getStashCount", LuaScriptInterface::luaPlayerGetStashCounter); + registerMethod("Player", "openStash", LuaScriptInterface::luaPlayerOpenStash); + registerMethod("Player", "addItemStash", LuaScriptInterface::luaPlayerAddItemStash); + registerMethod("Player", "removeStashItem", LuaScriptInterface::luaPlayerRemoveStashItem); // Monster registerClass(L, "Monster", "Creature", LuaScriptInterface::luaMonsterCreate); @@ -11301,6 +11307,100 @@ int LuaScriptInterface::luaPlayerSendEnterMarket(lua_State* L) return 1; } +int LuaScriptInterface::luaPlayerGetStashItemCount(lua_State* L) { + // player:getStashItemCount(itemId) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + const ItemType &itemType = Item::items[itemId]; + if (itemType.id == 0) { + lua_pushnil(L); + return 1; + } + lua_pushnumber(L, player->getStashItemCount(itemType.id)); + return 1; +} + +int LuaScriptInterface::luaPlayerGetStashCounter(lua_State* L) { + // player:getStashCount() + Player* player = getUserdata(L, 1); + if (player) { + uint16_t sizeStash = getStashSize(player->getStashItems()); + lua_pushnumber(L, sizeStash); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerOpenStash(lua_State* L) { + // player:openStash(isNpc) + Player* player = getUserdata(L, 1); + bool isNpc = getBoolean(L, 2, false); + if (player) { + player->sendOpenStash(isNpc); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddItemStash(lua_State* L) { + // player:addItemStash(itemId, clientId, count = 1) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + auto itemId = getNumber(L, 2); + auto clientId = getNumber(L, 3); + auto count = getNumber(L, 4, 1); // Default count to 1 if not provided + // Pass itemId, count, and clientId to the player's stash + player->addItemOnStash(itemId, count, clientId); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveStashItem(lua_State* L) { + // player:removeStashItem(itemId, count) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + const ItemType& itemType = Item::items[itemId]; + if (itemType.id == 0) { + lua_pushnil(L); + return 1; + } + uint32_t count = getNumber(L, 3); + pushBoolean(L, player->withdrawItem(itemType.id, count)); + return 1; +} + // Monster int LuaScriptInterface::luaMonsterCreate(lua_State* L) { diff --git a/src/luascript.h b/src/luascript.h index 9bafa7bc68..a43fdb8777 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -803,6 +803,12 @@ class LuaScriptInterface static int luaPlayerSendResourceBalance(lua_State* L); static int luaPlayerSendEnterMarket(lua_State* L); + + static int luaPlayerGetStashItemCount(lua_State* L); + static int luaPlayerGetStashCounter(lua_State* L); + static int luaPlayerOpenStash(lua_State* L); + static int luaPlayerAddItemStash(lua_State* L); + static int luaPlayerRemoveStashItem(lua_State* L); // Monster static int luaMonsterCreate(lua_State* L); diff --git a/src/player.cpp b/src/player.cpp index 356d04b837..ee4165d6ee 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -107,6 +107,19 @@ bool Player::setVocation(uint16_t vocId) return true; } +uint16_t Player::getFreeBackpackSlots() const { + Thing* thing = getThing(CONST_SLOT_BACKPACK); + if (!thing) { + return 0; + } + Container* backpack = thing->getContainer(); + if (!backpack) { + return 0; + } + uint16_t counter = std::max(0, backpack->getFreeSlots()); + return counter; +} + bool Player::isPushable() const { if (hasFlag(PlayerFlag_CannotBePushed)) { @@ -842,6 +855,7 @@ DepotLocker& Player::getDepotLocker() { if (!depotLocker) { depotLocker = std::make_shared(ITEM_LOCKER); + depotLocker->internalAddThing(Item::CreateItem(ITEM_SUPPLY_STASH)); depotLocker->internalAddThing(Item::CreateItem(ITEM_MARKET)); depotLocker->internalAddThing(getInbox().get()); @@ -3026,6 +3040,72 @@ uint32_t Player::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) con return count; } +void Player::stashContainer(StashContainerList itemDict) { + StashItemList stashItemDict; // ItemID - StashItem (clientId, itemCount) + //std::cout << "Initializing stashContainer with " << itemDict.size() << " items in itemDict." << std::endl; + for (const auto& it_dict : itemDict) { + uint16_t itemId = (it_dict.first)->getID(); + uint16_t clientId = (it_dict.first)->getClientID(); // Assuming getClientID is available + //std::cout << "Processing item: " << itemId << " with clientId: " << clientId << " and count: " << it_dict.second << std::endl; + // Add the item to stashItemDict with the itemId and itemCount + stashItemDict[itemId] = {clientId, it_dict.second}; + } + // Add existing stash items to stashItemDict, merging counts if the item already exists + for (auto it : stashItems) { + if (stashItemDict.find(it.first) == stashItemDict.end()) { + //std::cout << "Adding new item from existing stash: " << it.first << std::endl; + stashItemDict[it.first] = it.second; + } else { + //std::cout << "Merging existing item: " << it.first << " with new count: " << it.second.itemCount << std::endl; + stashItemDict[it.first].itemCount += it.second.itemCount; + } + } + // Check if the total stash size exceeds the capacity limit + uint32_t stashSize = getStashSize(stashItemDict); + //std::cout << "Total stash size after merging: " << stashSize << std::endl; + if (stashSize > g_config.getNumber(ConfigManager::STASH_ITEMS)) { + //std::cout << "Error: Stash size exceeds limit." << std::endl; + sendCancelMessage("You don't have capacity in the Supply Stash to stow all this item."); + return; + } + uint32_t totalStowed = 0; + std::ostringstream retString; + // Process items in itemDict and attempt to remove and stash them + for (const auto& stashIterator : itemDict) { + uint16_t itemId = (stashIterator.first)->getID(); // Extract item ID + uint16_t clientId = stashIterator.first->getClientID(); // Extract client ID + uint32_t amount = stashIterator.second; // Extract item amount + //std::cout << "Attempting to remove item: " << itemId << " with clientId: " << clientId << " and count: " << amount << std::endl; + // Try to remove the item from the container and add it to the stash + if (g_game.internalRemoveItem(stashIterator.first, amount) == RETURNVALUE_NOERROR) { + //std::cout << "Successfully removed item: " << itemId << " from container." << std::endl; + addItemOnStash(itemId, amount, clientId); // Pass id, amount, and clientId + totalStowed += amount; + const std::string itemName = Item::items[itemId].name; // Assuming item names are retrievable from the Item class + std::ostringstream individualMessage; + individualMessage << "Stowed " << amount << "x " << itemName << "."; + sendTextMessage(MESSAGE_STATUS_DEFAULT, individualMessage.str()); + } else { + //std::cout << "Failed to remove item: " << itemId << std::endl; + } + } + if (totalStowed == 0) { + //std::cout << "No items were stowed." << std::endl; + sendCancelMessage("Sorry, not possible."); + return; + } + // Generate the result message to send to the player + retString << "Total stowed: " << totalStowed << " object" << (totalStowed > 1 ? "s." : "."); + //std::cout << "Total stowed: " << totalStowed << std::endl; + if (moved) { + retString << " Moved " << movedItems << " object" << (movedItems > 1 ? "s." : "."); + //std::cout << "Moved items: " << movedItems << std::endl; + movedItems = 0; + } + sendTextMessage(MESSAGE_STATUS_DEFAULT, retString.str()); + //std::cout << "Stash container operation completed successfully." << std::endl; +} + bool Player::removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped /* = false*/) const { if (amount == 0) { diff --git a/src/player.h b/src/player.h index 59eb2e2fb7..1d40f865f1 100644 --- a/src/player.h +++ b/src/player.h @@ -313,6 +313,43 @@ class Player final : public Creature, public Cylinder void removeMessageBuffer(); bool removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped = false) const; + + void addItemOnStash(uint16_t itemId, uint32_t amount, uint16_t clientId) { + auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + it->second.itemCount += amount; // Update item count + } else { + // Add a new item with the given amount and clientId + stashItems[itemId] = {clientId, amount}; + } + } + + uint32_t getStashItemCount(uint16_t itemId) const { + auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + return it->second.itemCount; // Return item count + } + return 0; + } + + bool withdrawItem(uint16_t itemId, uint32_t amount) { + auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + if (it->second.itemCount > amount) { + it->second.itemCount -= amount; // Reduce item count + } else if (it->second.itemCount == amount) { + stashItems.erase(itemId); // Remove item if fully withdrawn + } else { + return false; // Not enough items to withdraw + } + return true; + } + return false; // Item not found + } + + StashItemList getStashItems() const { + return stashItems; + } uint32_t getCapacity() const { @@ -442,6 +479,10 @@ class Player final : public Creature, public Cylinder bool hasShield() const; bool isAttackable() const override; static bool lastHitIsPlayer(Creature* lastHitCreature); + + // stash functions + bool addItemFromStash(uint16_t itemId, uint32_t itemCount); + void stowItem(Item* item, uint32_t count, bool allItems); void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; void changeMana(int32_t manaChange); @@ -538,6 +579,8 @@ class Player final : public Creature, public Cylinder size_t getMaxVIPEntries() const; size_t getMaxDepotItems() const; + + uint16_t getFreeBackpackSlots() const; // tile // send methods @@ -1068,6 +1111,11 @@ class Player final : public Creature, public Cylinder client->writeToOutputBuffer(message); } } + void sendOpenStash(bool isNpc = false) { + if (client && ((getLastDepotId() != -1) || isNpc)) { + client->sendOpenStash(); + } + } void sendCombatAnalyzer(CombatType_t type, int32_t amount, DamageAnalyzerImpactType impactType, const std::string& target) { @@ -1164,6 +1212,7 @@ class Player final : public Creature, public Cylinder size_t getFirstIndex() const override; size_t getLastIndex() const override; uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override; + void stashContainer(StashContainerList itemDict); std::map& getAllItemTypeCount(std::map& countMap) const override; Thing* getThing(size_t index) const override; @@ -1270,6 +1319,8 @@ class Player final : public Creature, public Cylinder uint16_t clientExpDisplay = 100; uint16_t clientStaminaBonusDisplay = 100; uint16_t clientLowLevelBonusDisplay = 0; + StashItemList stashItems; // [ItemID] = amount + uint32_t movedItems = 0; uint8_t soul = 0; std::bitset<6> blessings; @@ -1286,6 +1337,8 @@ class Player final : public Creature, public Cylinder bool chaseMode = false; bool secureMode = false; bool inMarket = false; + bool supplyStash = false; // Menu option 'stow, stow container ...' + bool moved = false; bool wasMounted = false; bool ghostMode = false; bool pzLocked = false; diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index 8bfec0886f..ac93212749 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -542,6 +542,7 @@ void ProtocolGame::parsePacket(NetworkMessage& msg) // case 0x2C: break; // team finder (leader) // case 0x2D: break; // team finder (member) // case 0x28: break; // stash withdraw + case 0x28: parseStashWithdraw(msg); break; case 0x32: parseExtendedOpcode(msg); break; // otclient extended opcode @@ -3773,6 +3774,58 @@ void ProtocolGame::AddShopItem(NetworkMessage& msg, const ShopInfo& item) msg.add(std::max(item.sellPrice, 0)); } +void ProtocolGame::sendOpenStash() { + NetworkMessage msg; + msg.addByte(0x29); + StashItemList list = player->getStashItems(); + msg.add(list.size()); + for (auto item : list) { + msg.add(item.first); // itemId + msg.add(item.second.clientId); // clientId + msg.add(item.second.itemCount); // itemCount + } + msg.add(static_cast(g_config.getNumber(ConfigManager::STASH_ITEMS) - getStashSize(list))); + writeToOutputBuffer(msg); +} + +void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { + Supply_Stash_Actions_t action = static_cast(msg.getByte()); + switch (action) { + case SUPPLY_STASH_ACTION_STOW_ITEM: { + Position pos = msg.getPosition(); + uint16_t clientId = msg.get(); + uint32_t count = msg.get(); + uint8_t stackpos = msg.getByte(); + g_game.playerStowItem(player->getID(), pos, clientId, stackpos, count, false); + break; + } + case SUPPLY_STASH_ACTION_STOW_CONTAINER: { + Position pos = msg.getPosition(); + uint16_t clientId = msg.get(); + uint8_t stackpos = msg.getByte(); + g_game.playerStowItem(player->getID(), pos, clientId, stackpos, 0, false); + break; + } + case SUPPLY_STASH_ACTION_STOW_STACK: { + Position pos = msg.getPosition(); + uint16_t clientId = msg.get(); + uint8_t stackpos = msg.getByte(); + g_game.playerStowItem(player->getID(), pos, clientId, stackpos, 0, true); + break; + } + case SUPPLY_STASH_ACTION_WITHDRAW: { + uint16_t itemId = msg.get(); + uint32_t count = msg.get(); + uint8_t stackpos = msg.getByte(); + g_game.playerStashWithdraw(player->getID(), itemId, count, stackpos); + break; + } + default: + std::cout << "Unknown 'supply stash' action switch: " << static_cast(action) << std::endl; + break; + } +} + void ProtocolGame::parseExtendedOpcode(NetworkMessage& msg) { uint8_t opcode = msg.getByte(); diff --git a/src/protocolgame.h b/src/protocolgame.h index af58f16d3e..53d2a8d9a8 100644 --- a/src/protocolgame.h +++ b/src/protocolgame.h @@ -270,6 +270,8 @@ class ProtocolGame final : public Protocol // inventory void sendInventoryItem(slots_t slot, const Item* item); void sendItems(); + void sendOpenStash(); + void parseStashWithdraw(NetworkMessage &msg); // messages void sendModalWindow(const ModalWindow& modalWindow); diff --git a/src/tools.cpp b/src/tools.cpp index d950718c4f..74f5226cf1 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -6,6 +6,7 @@ #include "tools.h" #include "configmanager.h" +#include "item.h" #include #include @@ -134,6 +135,19 @@ std::string generateToken(std::string_view key, uint64_t counter, size_t length return token.substr(token.size() - length); } +uint16_t getStashSize(StashItemList itemList) { + uint16_t size = 0; + for (const auto& item : itemList) { + // Assume a default stack size of 1 for non-stackable items + uint32_t stackSize = Item::items[item.first].stackable ? 100 : 1; // Replace 100 with actual default stack size + // Access the item count from the stashItem (item.second) + uint32_t itemCount = item.second.itemCount; + // Calculate how many full stacks are needed and accumulate the total size + size += (itemCount + stackSize - 1) / stackSize; // This ensures rounding up without using float + } + return size; +} + bool caseInsensitiveEqual(std::string_view str1, std::string_view str2) { return str1.size() == str2.size() && diff --git a/src/tools.h b/src/tools.h index 97b2381718..a8b3c90041 100644 --- a/src/tools.h +++ b/src/tools.h @@ -14,6 +14,8 @@ std::string transformToSHA1(std::string_view input); std::string hmac(std::string_view algorithm, std::string_view key, std::string_view message); std::string generateToken(std::string_view key, uint64_t counter, size_t length = AUTHENTICATOR_DIGITS); +uint16_t getStashSize(StashItemList itemList); + // checks that str1 is equivalent to str2 ignoring letter case bool caseInsensitiveEqual(std::string_view str1, std::string_view str2); From 47fb5a4c87202e890e49ad7f58dca582e283a2fd Mon Sep 17 00:00:00 2001 From: jromap7 Date: Wed, 8 Jan 2025 13:47:31 +0100 Subject: [PATCH 2/3] added player.cpp missing --- src/player.cpp | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/player.cpp b/src/player.cpp index ee4165d6ee..d6bf865c84 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -4806,3 +4806,85 @@ void Player::updateRegeneration() condition->setParam(CONDITION_PARAM_MANATICKS, vocation->getManaGainTicks() * 1000); } } + +bool Player::addItemFromStash(uint16_t itemId, uint32_t itemCount) { + uint32_t stackCount = 100u; + while (itemCount > 0) { + auto addValue = itemCount > stackCount ? stackCount : itemCount; + itemCount -= addValue; + Item* newItem = Item::CreateItem(itemId, addValue); + + g_game.internalPlayerAddItem(this, newItem, true); + } + return true; +} + +void sendStowItems(const Item* item, const Item* stowItem, StashContainerList& itemDict) { + // Check if the item IDs match and push to itemDict + if (stowItem->getID() == item->getID()) { + // Cast stowItem to Item* before pushing to itemDict + itemDict.push_back(std::make_pair(const_cast(stowItem), stowItem->getItemCount())); + } + // If the stowItem is a container, iterate over its stowable items + if (const auto* container = stowItem->getContainer()) { + for (const auto& stowable_it : container->getStowableItems()) { + // Check if the stowable item's ID matches the item's ID + if (stowable_it.first->getID() == item->getID()) { + // Push the matching stowable item to itemDict + itemDict.push_back(std::make_pair(const_cast(stowable_it.first), stowable_it.second)); + } + } + } +} + +void Player::stowItem(Item* item, uint32_t count, bool allItems) { + // Check if the player is near a depot box + if (!isNearDepotBox()) { + sendCancelMessage("You need to be nearby depot to stow items."); + return; + } + // Check if the item is valid + if (!item) { + sendCancelMessage("This item cannot be stowed here."); + return; + } + // List of items that cannot be stowed (coin types) + const std::set excludedItemIds = { + ITEM_GOLD_COIN, + ITEM_PLATINUM_COIN, + ITEM_CRYSTAL_COIN + }; + StashContainerList itemDict; + // If the item is a container, process its contents + if (item->getContainer()) { + itemDict = item->getContainer()->getStowableItems(); + // Iterate through container items and move non-stackable items (if configured) + for (Item* containerItem : item->getContainer()->getItems(true)) { + if (g_config.getBoolean(ConfigManager::STASH_MOVING) && containerItem && !containerItem->isStackable()) { + // Move non-stackable items if not excluded + g_game.internalMoveItem(containerItem->getParent(), getDepotChest(getLastDepotId(), true), INDEX_WHEREEVER, containerItem, containerItem->getItemCount(), nullptr); + movedItems++; + moved = true; + } + } + } else { + // If the item is not a container, check if it's stackable + if (!item->isStackable()) { + sendCancelMessage("This item cannot be stowed here because it is not stackable."); + return; + } + // Add the item to the stash if it is not excluded + itemDict.emplace_back(item, count); + } + // Excluded items check: filter out any item from the itemDict if it matches an excluded item ID + itemDict.erase(std::remove_if(itemDict.begin(), itemDict.end(), [&](const std::pair& pair) { + return excludedItemIds.find(pair.first->getID()) != excludedItemIds.end(); + }), itemDict.end()); + // If no stowable items were found, send a specific cancel message + if (itemDict.empty()) { + sendCancelMessage("There are no stowable items."); + return; + } + // Stow the items + stashContainer(itemDict); +} \ No newline at end of file From ecc57cdcc33ad7c964579423fef8072f0059ad81 Mon Sep 17 00:00:00 2001 From: jromap7 Date: Sun, 19 Jan 2025 15:42:26 +0100 Subject: [PATCH 3/3] added migrations, schema, refactored code --- data/migrations/37.lua | 16 ++++- data/migrations/38.lua | 3 + schema.sql | 8 +++ src/enums.h | 6 +- src/game.cpp | 94 ++++++++++++++------------- src/iologindata.cpp | 24 +++---- src/luascript.cpp | 12 ++-- src/player.cpp | 140 ++++++++++++++++++++++------------------- src/player.h | 28 ++++----- src/protocolgame.cpp | 70 +++++++++++++++++---- src/tools.cpp | 11 ++-- src/tools.h | 2 +- 12 files changed, 250 insertions(+), 164 deletions(-) create mode 100644 data/migrations/38.lua diff --git a/data/migrations/37.lua b/data/migrations/37.lua index d0ffd9c0cb..4a1d96d2f5 100644 --- a/data/migrations/37.lua +++ b/data/migrations/37.lua @@ -1,3 +1,15 @@ function onUpdateDatabase() - return false -end + print("> Updating database to version 38 (player stash)") + + db.query([[ + CREATE TABLE IF NOT EXISTS `player_stash` ( + `player_id` INT NOT NULL, + `item_id` SMALLINT UNSIGNED NOT NULL, + `item_count` INT UNSIGNED NOT NULL, + PRIMARY KEY (`player_id`, `item_id`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Stores the items stashed by players'; + ]]) + + return true +end \ No newline at end of file diff --git a/data/migrations/38.lua b/data/migrations/38.lua new file mode 100644 index 0000000000..53778c6741 --- /dev/null +++ b/data/migrations/38.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false +end \ No newline at end of file diff --git a/schema.sql b/schema.sql index b41feca8a6..f64543f021 100644 --- a/schema.sql +++ b/schema.sql @@ -328,6 +328,14 @@ CREATE TABLE IF NOT EXISTS `player_items` ( KEY `sid` (`sid`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; +CREATE TABLE IF NOT EXISTS `player_stash` ( + `player_id` int NOT NULL, -- The unique ID of the player + `item_id` smallint unsigned NOT NULL, -- The ID of the stashed item + `item_count` int unsigned NOT NULL, -- The number of items stashed + PRIMARY KEY (`player_id`, `item_id`), -- Ensures each player can only have one entry per item_id + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CREATE TABLE IF NOT EXISTS `player_spells` ( `player_id` int NOT NULL, `name` varchar(255) NOT NULL, diff --git a/src/enums.h b/src/enums.h index 8fcd6ef5b0..c72fa32ea9 100644 --- a/src/enums.h +++ b/src/enums.h @@ -655,11 +655,7 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ShopInfoList = std::list; -struct StashItem { - uint16_t clientId; - uint32_t itemCount; -}; -using StashItemList = std::map; // itemId, StashItem +using StashItemList = std::map; // itemID, count enum MonstersEvent_t : uint8_t { diff --git a/src/game.cpp b/src/game.cpp index 5a986598af..7c68755bc8 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2554,52 +2554,50 @@ void Game::playerBrowseField(uint32_t playerId, const Position& pos) } } -void Game::playerStowItem(uint32_t playerId, const Position &pos, uint16_t clientId, uint8_t stackpos, uint8_t count, bool allItems) { +void Game::playerStowItem(uint32_t playerId, const Position& pos, uint16_t itemId, uint8_t stackpos, uint8_t count, bool allItems) { Player* player = getPlayerByID(playerId); if (!player) { - //std::cout << "Error: Player not found for ID: " << playerId << std::endl; return; } + if (!player->isPremium()) { - //std::cout << "Error: Player does not have a premium account." << std::endl; player->sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT); return; } - // Get the itemId using clientId - const ItemType& it = Item::items.getItemIdByClientId(clientId); - if (it.id == 0) { - //std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; - return; - } - uint16_t itemId = it.id; // Reassign itemId from the retrieved ItemType + Thing* thing = internalGetThing(player, pos, stackpos, itemId, STACKPOS_TOPDOWN_ITEM); if (!thing) { - //std::cout << "Error: Thing not found at position: " << pos << " with stackpos: " << (int)stackpos << " and itemId: " << itemId << std::endl; player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } + Item* item = thing->getItem(); if (!item) { - //std::cout << "Error: Thing is not an item." << std::endl; player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } + + // Use tryRetrieveStashItems for additional validation + if (!tryRetrieveStashItems(player, item)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + if (item->getID() != itemId) { - //std::cout << "Error: Item ID mismatch. Expected: " << itemId << " but got: " << item->getID() << std::endl; player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } + if (item->getItemCount() < count) { - //std::cout << "Error: Not enough items to stow. Requested count: " << (int)count << " but only have: " << item->getItemCount() << std::endl; player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } + if (pos.x != 0xFFFF && !Position::areInRange<1, 1, 0>(pos, player->getPosition())) { - //std::cout << "Error: Player is not in range. Player position: " << player->getPosition() << " Item position: " << pos << std::endl; player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } - //std::cout << "Success: Stowing item. Item ID: " << itemId << " Count: " << (int)count << " All items: " << allItems << std::endl; + player->stowItem(item, count, allItems); } @@ -2609,53 +2607,59 @@ void Game::playerStashWithdraw(uint32_t playerId, uint16_t itemId, uint32_t coun return; } if (player->hasFlag(PlayerFlags::PlayerFlag_CannotPickupItem)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } - const ItemType &it = Item::items[itemId]; + + const ItemType& it = Item::items[itemId]; if (it.id == 0 || count == 0) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); return; } + uint16_t freeSlots = player->getFreeBackpackSlots(); if (freeSlots == 0) { player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); return; } - if (player->getFreeCapacity() < 100) { + + if (player->getFreeCapacity() < (count * it.weight)) { player->sendCancelMessage(RETURNVALUE_NOTENOUGHCAPACITY); return; } - - // Assume stack size of 100 for stackable items, and 1 for non-stackable + + // Determine stack size: 100 for stackable items, 1 for non-stackable uint32_t stackSize = it.stackable ? 100 : 1; - int32_t NDSlots = ((freeSlots) - (count < stackSize ? 1 : (count / stackSize))); - uint32_t SlotsWith = count; - uint32_t noSlotsWith = 0; - if (NDSlots <= 0) { - SlotsWith = (freeSlots * stackSize); - noSlotsWith = (count - SlotsWith); - } - uint32_t capWith = count; - uint32_t noCapWith = 0; - if (player->getFreeCapacity() < (count * it.weight)) { - capWith = (player->getFreeCapacity() / it.weight); - noCapWith = (count - capWith); - } - std::stringstream ss; - uint32_t WithdrawCount = (SlotsWith > capWith ? capWith : SlotsWith); - uint32_t NoWithdrawCount = (noSlotsWith < noCapWith ? noCapWith : noSlotsWith); - const char* NoWithdrawMsg = (noSlotsWith < noCapWith ? "capacity" : "slots"); - if (WithdrawCount != count) { - ss << "Retrieved " << WithdrawCount << "x " << it.name << ".\n"; - ss << NoWithdrawCount << "x are impossible to retrieve due to insufficient inventory " << NoWithdrawMsg << "."; - } else { - ss << "Retrieved " << WithdrawCount << "x " << it.name << '.'; + + // Calculate how many slots are needed and compare with free slots + uint32_t requiredSlots = (count + stackSize - 1) / stackSize; // Ceiling division for slots + if (requiredSlots > freeSlots) { + count = freeSlots * stackSize; // Adjust count to fit available slots } - player->sendTextMessage(MESSAGE_STATUS_DEFAULT, ss.str()); - if (player->withdrawItem(itemId, WithdrawCount)) { - player->addItemFromStash(it.id, WithdrawCount); + + // Calculate how much weight the player can carry + uint32_t maxWeightCount = player->getFreeCapacity() / it.weight; + if (count > maxWeightCount) { + count = maxWeightCount; // Adjust count to fit capacity + } + + if (count == 0) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + // Withdraw items from stash and add them to the player's inventory + if (player->withdrawItem(itemId, count)) { + player->addItemFromStash(it.id, count); + + std::stringstream ss; + ss << "Retrieved " << count << "x " << it.name << "."; + player->sendTextMessage(MESSAGE_STATUS_DEFAULT, ss.str()); } else { player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); } + + // Update stash view for the player player->sendOpenStash(true); } diff --git a/src/iologindata.cpp b/src/iologindata.cpp index f27008ac99..367805dbca 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -487,16 +487,16 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) } } - // load stash items + // Load stash items query.str(std::string()); - query << "SELECT `item_count`, `item_id`, `client_id` FROM `player_stash` WHERE `player_id` = " << player->getGUID(); + query << "SELECT `item_count`, `item_id` FROM `player_stash` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { uint16_t itemId = result->getNumber("item_id"); uint32_t itemCount = result->getNumber("item_count"); - uint16_t clientId = result->getNumber("client_id"); - // Now add the item to the player's stash with both itemCount and clientId - player->addItemOnStash(itemId, itemCount, clientId); + + // Add the item to the player's stash with itemId and itemCount + player->addItemOnStash(itemId, itemCount); } while (result->next()); } @@ -762,19 +762,21 @@ bool IOLoginData::savePlayer(Player* player) if (!db.executeQuery(query.str())) { return false; } + query.str(std::string()); - DBInsert stashQuery("INSERT INTO `player_stash` (`player_id`, `item_id`, `item_count`, `client_id`) VALUES "); + DBInsert stashQuery("INSERT INTO `player_stash` (`player_id`, `item_id`, `item_count`) VALUES "); for (const auto& stashItem : player->getStashItems()) { - uint16_t itemId = stashItem.first; // Accessing the key (itemId) - uint32_t itemCount = stashItem.second.itemCount; // Accessing the item count - uint16_t clientId = stashItem.second.clientId; // Accessing the clientId + uint16_t itemId = stashItem.first; // Accessing the key (itemId) + uint32_t itemCount = stashItem.second; // Accessing the item count + query.str(std::string()); - query << player->getGUID() << ',' << itemId << ',' << itemCount << ',' << clientId; - + query << player->getGUID() << ',' << itemId << ',' << itemCount; + if (!stashQuery.addRow(query)) { return false; } } + if (!stashQuery.execute()) { return false; } diff --git a/src/luascript.cpp b/src/luascript.cpp index 270cc23fee..ef7383f94f 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -11359,17 +11359,19 @@ int LuaScriptInterface::luaPlayerOpenStash(lua_State* L) { } int LuaScriptInterface::luaPlayerAddItemStash(lua_State* L) { - // player:addItemStash(itemId, clientId, count = 1) + // player:addItemStash(itemId, count = 1) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } + auto itemId = getNumber(L, 2); - auto clientId = getNumber(L, 3); - auto count = getNumber(L, 4, 1); // Default count to 1 if not provided - // Pass itemId, count, and clientId to the player's stash - player->addItemOnStash(itemId, count, clientId); + auto count = getNumber(L, 3, 1); // Default count to 1 if not provided + + // Add itemId and count to the player's stash + player->addItemOnStash(itemId, count); + pushBoolean(L, true); return 1; } diff --git a/src/player.cpp b/src/player.cpp index d6bf865c84..2c1724e406 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -3041,69 +3041,56 @@ uint32_t Player::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) con } void Player::stashContainer(StashContainerList itemDict) { - StashItemList stashItemDict; // ItemID - StashItem (clientId, itemCount) - //std::cout << "Initializing stashContainer with " << itemDict.size() << " items in itemDict." << std::endl; + StashItemList stashItemDict; // ItemID -> count mapping + for (const auto& it_dict : itemDict) { uint16_t itemId = (it_dict.first)->getID(); - uint16_t clientId = (it_dict.first)->getClientID(); // Assuming getClientID is available - //std::cout << "Processing item: " << itemId << " with clientId: " << clientId << " and count: " << it_dict.second << std::endl; - // Add the item to stashItemDict with the itemId and itemCount - stashItemDict[itemId] = {clientId, it_dict.second}; + uint32_t itemCount = it_dict.second; + + // Add the item to stashItemDict + stashItemDict[itemId] += itemCount; // Merge counts if the item already exists } - // Add existing stash items to stashItemDict, merging counts if the item already exists - for (auto it : stashItems) { - if (stashItemDict.find(it.first) == stashItemDict.end()) { - //std::cout << "Adding new item from existing stash: " << it.first << std::endl; - stashItemDict[it.first] = it.second; - } else { - //std::cout << "Merging existing item: " << it.first << " with new count: " << it.second.itemCount << std::endl; - stashItemDict[it.first].itemCount += it.second.itemCount; - } + + // Merge existing stash items + for (const auto& it : stashItems) { + stashItemDict[it.first] += it.second; // Merge item counts } + // Check if the total stash size exceeds the capacity limit uint32_t stashSize = getStashSize(stashItemDict); - //std::cout << "Total stash size after merging: " << stashSize << std::endl; if (stashSize > g_config.getNumber(ConfigManager::STASH_ITEMS)) { - //std::cout << "Error: Stash size exceeds limit." << std::endl; - sendCancelMessage("You don't have capacity in the Supply Stash to stow all this item."); + sendCancelMessage("You don't have capacity in the Supply Stash to stow all these items."); return; } + uint32_t totalStowed = 0; - std::ostringstream retString; + // Process items in itemDict and attempt to remove and stash them for (const auto& stashIterator : itemDict) { - uint16_t itemId = (stashIterator.first)->getID(); // Extract item ID - uint16_t clientId = stashIterator.first->getClientID(); // Extract client ID - uint32_t amount = stashIterator.second; // Extract item amount - //std::cout << "Attempting to remove item: " << itemId << " with clientId: " << clientId << " and count: " << amount << std::endl; + uint16_t itemId = stashIterator.first->getID(); // Extract item ID + uint32_t amount = stashIterator.second; // Extract item amount + // Try to remove the item from the container and add it to the stash if (g_game.internalRemoveItem(stashIterator.first, amount) == RETURNVALUE_NOERROR) { - //std::cout << "Successfully removed item: " << itemId << " from container." << std::endl; - addItemOnStash(itemId, amount, clientId); // Pass id, amount, and clientId + addItemOnStash(itemId, amount); // Add itemId and count to stash totalStowed += amount; - const std::string itemName = Item::items[itemId].name; // Assuming item names are retrievable from the Item class - std::ostringstream individualMessage; - individualMessage << "Stowed " << amount << "x " << itemName << "."; - sendTextMessage(MESSAGE_STATUS_DEFAULT, individualMessage.str()); - } else { - //std::cout << "Failed to remove item: " << itemId << std::endl; + + const std::string itemName = Item::items[itemId].name; // Retrieve item name + std::ostringstream individualMessage; + individualMessage << "Stowed " << amount << "x " << itemName << "."; + sendTextMessage(MESSAGE_STATUS_DEFAULT, individualMessage.str()); } } + if (totalStowed == 0) { - //std::cout << "No items were stowed." << std::endl; sendCancelMessage("Sorry, not possible."); return; } - // Generate the result message to send to the player + + // Send a summary message to the player + std::ostringstream retString; retString << "Total stowed: " << totalStowed << " object" << (totalStowed > 1 ? "s." : "."); - //std::cout << "Total stowed: " << totalStowed << std::endl; - if (moved) { - retString << " Moved " << movedItems << " object" << (movedItems > 1 ? "s." : "."); - //std::cout << "Moved items: " << movedItems << std::endl; - movedItems = 0; - } sendTextMessage(MESSAGE_STATUS_DEFAULT, retString.str()); - //std::cout << "Stash container operation completed successfully." << std::endl; } bool Player::removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped /* = false*/) const @@ -4820,18 +4807,19 @@ bool Player::addItemFromStash(uint16_t itemId, uint32_t itemCount) { } void sendStowItems(const Item* item, const Item* stowItem, StashContainerList& itemDict) { - // Check if the item IDs match and push to itemDict + // Check if the item IDs match if (stowItem->getID() == item->getID()) { - // Cast stowItem to Item* before pushing to itemDict - itemDict.push_back(std::make_pair(const_cast(stowItem), stowItem->getItemCount())); + // Add the stowItem to itemDict + itemDict.emplace_back(const_cast(stowItem), stowItem->getItemCount()); } - // If the stowItem is a container, iterate over its stowable items + + // If stowItem is a container, iterate over its items if (const auto* container = stowItem->getContainer()) { - for (const auto& stowable_it : container->getStowableItems()) { + for (const auto& stowableItem : container->getStowableItems()) { // Check if the stowable item's ID matches the item's ID - if (stowable_it.first->getID() == item->getID()) { - // Push the matching stowable item to itemDict - itemDict.push_back(std::make_pair(const_cast(stowable_it.first), stowable_it.second)); + if (stowableItem.first->getID() == item->getID()) { + // Add the stowable item to itemDict + itemDict.emplace_back(const_cast(stowableItem.first), stowableItem.second); } } } @@ -4840,51 +4828,73 @@ void sendStowItems(const Item* item, const Item* stowItem, StashContainerList& i void Player::stowItem(Item* item, uint32_t count, bool allItems) { // Check if the player is near a depot box if (!isNearDepotBox()) { - sendCancelMessage("You need to be nearby depot to stow items."); + sendCancelMessage("You need to be nearby a depot to stow items."); return; } + // Check if the item is valid if (!item) { sendCancelMessage("This item cannot be stowed here."); return; } - // List of items that cannot be stowed (coin types) + + // List of excluded item IDs (coins) const std::set excludedItemIds = { ITEM_GOLD_COIN, ITEM_PLATINUM_COIN, ITEM_CRYSTAL_COIN }; + StashContainerList itemDict; + // If the item is a container, process its contents if (item->getContainer()) { + // Get stowable items from the container itemDict = item->getContainer()->getStowableItems(); - // Iterate through container items and move non-stackable items (if configured) - for (Item* containerItem : item->getContainer()->getItems(true)) { - if (g_config.getBoolean(ConfigManager::STASH_MOVING) && containerItem && !containerItem->isStackable()) { - // Move non-stackable items if not excluded - g_game.internalMoveItem(containerItem->getParent(), getDepotChest(getLastDepotId(), true), INDEX_WHEREEVER, containerItem, containerItem->getItemCount(), nullptr); - movedItems++; - moved = true; + + // Move non-stackable items to the depot if configured + if (g_config.getBoolean(ConfigManager::STASH_MOVING)) { + for (Item* containerItem : item->getContainer()->getItems(true)) { + if (containerItem && !containerItem->isStackable()) { + g_game.internalMoveItem( + containerItem->getParent(), + getDepotChest(getLastDepotId(), true), + INDEX_WHEREEVER, + containerItem, + containerItem->getItemCount(), + nullptr + ); + movedItems++; + moved = true; + } } } } else { - // If the item is not a container, check if it's stackable + // If the item is not a container, ensure it's stackable if (!item->isStackable()) { - sendCancelMessage("This item cannot be stowed here because it is not stackable."); + sendCancelMessage("This item cannot be stowed because it is not stackable."); return; } - // Add the item to the stash if it is not excluded + + // Add the item to the stash if it's not excluded itemDict.emplace_back(item, count); } - // Excluded items check: filter out any item from the itemDict if it matches an excluded item ID - itemDict.erase(std::remove_if(itemDict.begin(), itemDict.end(), [&](const std::pair& pair) { - return excludedItemIds.find(pair.first->getID()) != excludedItemIds.end(); - }), itemDict.end()); - // If no stowable items were found, send a specific cancel message + + // Filter out excluded items + itemDict.erase(std::remove_if( + itemDict.begin(), + itemDict.end(), + [&](const std::pair& pair) { + return excludedItemIds.find(pair.first->getID()) != excludedItemIds.end(); + } + ), itemDict.end()); + + // If no stowable items remain, send a cancel message if (itemDict.empty()) { sendCancelMessage("There are no stowable items."); return; } + // Stow the items stashContainer(itemDict); } \ No newline at end of file diff --git a/src/player.h b/src/player.h index 1d40f865f1..8aabae7cc3 100644 --- a/src/player.h +++ b/src/player.h @@ -314,37 +314,37 @@ class Player final : public Creature, public Cylinder bool removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped = false) const; - void addItemOnStash(uint16_t itemId, uint32_t amount, uint16_t clientId) { + void addItemOnStash(uint16_t itemId, uint32_t amount) { auto it = stashItems.find(itemId); if (it != stashItems.end()) { - it->second.itemCount += amount; // Update item count + it->second += amount; } else { - // Add a new item with the given amount and clientId - stashItems[itemId] = {clientId, amount}; + stashItems[itemId] = amount; } } + uint32_t getStashItemCount(uint16_t itemId) const { - auto it = stashItems.find(itemId); + const auto it = stashItems.find(itemId); if (it != stashItems.end()) { - return it->second.itemCount; // Return item count + return it->second; } return 0; } bool withdrawItem(uint16_t itemId, uint32_t amount) { - auto it = stashItems.find(itemId); + const auto it = stashItems.find(itemId); if (it != stashItems.end()) { - if (it->second.itemCount > amount) { - it->second.itemCount -= amount; // Reduce item count - } else if (it->second.itemCount == amount) { - stashItems.erase(itemId); // Remove item if fully withdrawn + if (it->second > amount) { + stashItems[itemId] -= amount; + } else if (it->second == amount) { + stashItems.erase(itemId); } else { - return false; // Not enough items to withdraw + return false; } return true; } - return false; // Item not found + return false; } StashItemList getStashItems() const { @@ -1212,7 +1212,7 @@ class Player final : public Creature, public Cylinder size_t getFirstIndex() const override; size_t getLastIndex() const override; uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override; - void stashContainer(StashContainerList itemDict); + void stashContainer(const StashContainerList &itemDict); std::map& getAllItemTypeCount(std::map& countMap) const override; Thing* getThing(size_t index) const override; diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index ac93212749..97be2c6809 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -3776,18 +3776,28 @@ void ProtocolGame::AddShopItem(NetworkMessage& msg, const ShopInfo& item) void ProtocolGame::sendOpenStash() { NetworkMessage msg; - msg.addByte(0x29); + msg.addByte(0x29); // Stash open opcode StashItemList list = player->getStashItems(); - msg.add(list.size()); - for (auto item : list) { - msg.add(item.first); // itemId - msg.add(item.second.clientId); // clientId - msg.add(item.second.itemCount); // itemCount + msg.add(list.size()); // Number of items in stash + + for (const auto& item : list) { + const ItemType& it = Item::items.getItemType(item.first); // Get ItemType from itemId + if (it.clientId == 0) { + std::cout << "Error: Invalid itemId: " << item.first << ". No matching clientId found." << std::endl; + continue; // Skip invalid entries + } + + msg.add(it.clientId); // Add clientId to message + msg.add(item.second); // Add itemCount to message } - msg.add(static_cast(g_config.getNumber(ConfigManager::STASH_ITEMS) - getStashSize(list))); + + uint16_t maxStashItems = static_cast(g_config.getNumber(ConfigManager::STASH_ITEMS)); + uint16_t stashSize = getStashSize(list); + msg.add(maxStashItems - stashSize); // Remaining stash capacity writeToOutputBuffer(msg); } + void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { Supply_Stash_Actions_t action = static_cast(msg.getByte()); switch (action) { @@ -3796,27 +3806,64 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { uint16_t clientId = msg.get(); uint32_t count = msg.get(); uint8_t stackpos = msg.getByte(); - g_game.playerStowItem(player->getID(), pos, clientId, stackpos, count, false); + + // Convert clientId to itemId + const ItemType& it = Item::items.getItemIdByClientId(clientId); + if (it.id == 0) { + // Invalid clientId, no matching item found + std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; + return; + } + uint16_t itemId = it.id; + + g_game.playerStowItem(player->getID(), pos, itemId, stackpos, count, false); break; } case SUPPLY_STASH_ACTION_STOW_CONTAINER: { Position pos = msg.getPosition(); uint16_t clientId = msg.get(); uint8_t stackpos = msg.getByte(); - g_game.playerStowItem(player->getID(), pos, clientId, stackpos, 0, false); + + // Convert clientId to itemId + const ItemType& it = Item::items.getItemIdByClientId(clientId); + if (it.id == 0) { + std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; + return; + } + uint16_t itemId = it.id; + + g_game.playerStowItem(player->getID(), pos, itemId, stackpos, 0, false); break; } case SUPPLY_STASH_ACTION_STOW_STACK: { Position pos = msg.getPosition(); uint16_t clientId = msg.get(); uint8_t stackpos = msg.getByte(); - g_game.playerStowItem(player->getID(), pos, clientId, stackpos, 0, true); + + // Convert clientId to itemId + const ItemType& it = Item::items.getItemIdByClientId(clientId); + if (it.id == 0) { + std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; + return; + } + uint16_t itemId = it.id; + + g_game.playerStowItem(player->getID(), pos, itemId, stackpos, 0, true); break; } case SUPPLY_STASH_ACTION_WITHDRAW: { - uint16_t itemId = msg.get(); + uint16_t clientId = msg.get(); uint32_t count = msg.get(); uint8_t stackpos = msg.getByte(); + + // Convert clientId to itemId + const ItemType& it = Item::items.getItemIdByClientId(clientId); + if (it.id == 0) { + std::cout << "Error: Invalid clientId: " << clientId << ". No matching item found." << std::endl; + return; + } + uint16_t itemId = it.id; + g_game.playerStashWithdraw(player->getID(), itemId, count, stackpos); break; } @@ -3826,6 +3873,7 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { } } + void ProtocolGame::parseExtendedOpcode(NetworkMessage& msg) { uint8_t opcode = msg.getByte(); diff --git a/src/tools.cpp b/src/tools.cpp index 74f5226cf1..0fc067e7b2 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -135,19 +135,20 @@ std::string generateToken(std::string_view key, uint64_t counter, size_t length return token.substr(token.size() - length); } -uint16_t getStashSize(StashItemList itemList) { +uint16_t getStashSize(const StashItemList& itemList) { uint16_t size = 0; for (const auto& item : itemList) { // Assume a default stack size of 1 for non-stackable items uint32_t stackSize = Item::items[item.first].stackable ? 100 : 1; // Replace 100 with actual default stack size - // Access the item count from the stashItem (item.second) - uint32_t itemCount = item.second.itemCount; - // Calculate how many full stacks are needed and accumulate the total size - size += (itemCount + stackSize - 1) / stackSize; // This ensures rounding up without using float + // Access the item count directly (item.second) + uint32_t itemCount = item.second; + // Calculate the number of stacks needed and accumulate the total size + size += (itemCount + stackSize - 1) / stackSize; // Rounds up to the nearest stack } return size; } + bool caseInsensitiveEqual(std::string_view str1, std::string_view str2) { return str1.size() == str2.size() && diff --git a/src/tools.h b/src/tools.h index a8b3c90041..cef0d1cbcf 100644 --- a/src/tools.h +++ b/src/tools.h @@ -14,7 +14,7 @@ std::string transformToSHA1(std::string_view input); std::string hmac(std::string_view algorithm, std::string_view key, std::string_view message); std::string generateToken(std::string_view key, uint64_t counter, size_t length = AUTHENTICATOR_DIGITS); -uint16_t getStashSize(StashItemList itemList); +uint16_t getStashSize(const StashItemList& itemList); // checks that str1 is equivalent to str2 ignoring letter case bool caseInsensitiveEqual(std::string_view str1, std::string_view str2);