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/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..c72fa32ea9 100644 --- a/src/enums.h +++ b/src/enums.h @@ -655,6 +655,8 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ShopInfoList = std::list; +using StashItemList = std::map; // itemID, count + enum MonstersEvent_t : uint8_t { MONSTERS_EVENT_NONE = 0, diff --git a/src/game.cpp b/src/game.cpp index 9a871adb52..7c68755bc8 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2554,6 +2554,115 @@ void Game::playerBrowseField(uint32_t playerId, const Position& pos) } } +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) { + return; + } + + if (!player->isPremium()) { + player->sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT); + return; + } + + Thing* thing = internalGetThing(player, pos, stackpos, itemId, STACKPOS_TOPDOWN_ITEM); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Item* item = thing->getItem(); + if (!item) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + // Use tryRetrieveStashItems for additional validation + if (!tryRetrieveStashItems(player, item)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (item->getID() != itemId) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (item->getItemCount() < count) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (pos.x != 0xFFFF && !Position::areInRange<1, 1, 0>(pos, player->getPosition())) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + 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)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + 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() < (count * it.weight)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHCAPACITY); + return; + } + + // Determine stack size: 100 for stackable items, 1 for non-stackable + uint32_t stackSize = it.stackable ? 100 : 1; + + // 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 + } + + // 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); +} + void Game::playerSeekInContainer(uint32_t playerId, uint8_t containerId, uint16_t index) { Player* player = getPlayerByID(playerId); @@ -5922,3 +6031,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..367805dbca 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` 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"); + + // Add the item to the player's stash with itemId and itemCount + player->addItemOnStash(itemId, itemCount); + } while (result->next()); + } // load storage map if ((result = db.storeQuery( @@ -742,6 +755,31 @@ 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`) VALUES "); + for (const auto& stashItem : player->getStashItems()) { + 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; + + 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..ef7383f94f 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,102 @@ 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, count = 1) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + auto itemId = getNumber(L, 2); + 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; +} + +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..2c1724e406 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,59 @@ uint32_t Player::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) con return count; } +void Player::stashContainer(StashContainerList itemDict) { + StashItemList stashItemDict; // ItemID -> count mapping + + for (const auto& it_dict : itemDict) { + uint16_t itemId = (it_dict.first)->getID(); + uint32_t itemCount = it_dict.second; + + // Add the item to stashItemDict + stashItemDict[itemId] += itemCount; // Merge counts if the item already exists + } + + // 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); + if (stashSize > g_config.getNumber(ConfigManager::STASH_ITEMS)) { + sendCancelMessage("You don't have capacity in the Supply Stash to stow all these items."); + return; + } + + uint32_t totalStowed = 0; + + // 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 + 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) { + addItemOnStash(itemId, amount); // Add itemId and count to stash + totalStowed += amount; + + 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) { + sendCancelMessage("Sorry, not possible."); + return; + } + + // Send a summary message to the player + std::ostringstream retString; + retString << "Total stowed: " << totalStowed << " object" << (totalStowed > 1 ? "s." : "."); + sendTextMessage(MESSAGE_STATUS_DEFAULT, retString.str()); +} + bool Player::removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped /* = false*/) const { if (amount == 0) { @@ -4726,3 +4793,108 @@ 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 + if (stowItem->getID() == item->getID()) { + // Add the stowItem to itemDict + itemDict.emplace_back(const_cast(stowItem), stowItem->getItemCount()); + } + + // If stowItem is a container, iterate over its items + if (const auto* container = stowItem->getContainer()) { + for (const auto& stowableItem : container->getStowableItems()) { + // Check if the stowable item's ID matches the item's ID + if (stowableItem.first->getID() == item->getID()) { + // Add the stowable item to itemDict + itemDict.emplace_back(const_cast(stowableItem.first), stowableItem.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 a depot to stow items."); + return; + } + + // Check if the item is valid + if (!item) { + sendCancelMessage("This item cannot be stowed here."); + return; + } + + // 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(); + + // 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, ensure it's stackable + if (!item->isStackable()) { + sendCancelMessage("This item cannot be stowed because it is not stackable."); + return; + } + + // Add the item to the stash if it's not excluded + itemDict.emplace_back(item, count); + } + + // 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 59eb2e2fb7..8aabae7cc3 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) { + auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + it->second += amount; + } else { + stashItems[itemId] = amount; + } + } + + + uint32_t getStashItemCount(uint16_t itemId) const { + const auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + return it->second; + } + return 0; + } + + bool withdrawItem(uint16_t itemId, uint32_t amount) { + const auto it = stashItems.find(itemId); + if (it != stashItems.end()) { + if (it->second > amount) { + stashItems[itemId] -= amount; + } else if (it->second == amount) { + stashItems.erase(itemId); + } else { + return false; + } + return true; + } + return false; + } + + 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(const 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..97be2c6809 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,106 @@ void ProtocolGame::AddShopItem(NetworkMessage& msg, const ShopInfo& item) msg.add(std::max(item.sellPrice, 0)); } +void ProtocolGame::sendOpenStash() { + NetworkMessage msg; + msg.addByte(0x29); // Stash open opcode + StashItemList list = player->getStashItems(); + 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 + } + + 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) { + 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(); + + // 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(); + + // 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(); + + // 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 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; + } + 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..0fc067e7b2 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,20 @@ std::string generateToken(std::string_view key, uint64_t counter, size_t length return token.substr(token.size() - length); } +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 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 97b2381718..cef0d1cbcf 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(const StashItemList& itemList); + // checks that str1 is equivalent to str2 ignoring letter case bool caseInsensitiveEqual(std::string_view str1, std::string_view str2);