diff --git a/data/lib/core/container.lua b/data/lib/core/container.lua index 14d516ffec..f768e33b24 100644 --- a/data/lib/core/container.lua +++ b/data/lib/core/container.lua @@ -20,7 +20,7 @@ function Container.createLootItem(self, item) end while itemCount > 0 do - local count = math.min(ITEM_STACK_SIZE, itemCount) + local count = math.min(itemType:getStackSize(), itemCount) local subType = count if itemType:isFluidContainer() then diff --git a/data/npc/lib/npc.lua b/data/npc/lib/npc.lua index 484c410e6e..2d07ce1abb 100644 --- a/data/npc/lib/npc.lua +++ b/data/npc/lib/npc.lua @@ -11,19 +11,56 @@ function msgcontains(message, keyword) return message:find(keyword) and not message:find('(%w+)' .. keyword) end + function doNpcSellItem(cid, itemid, amount, subType, ignoreCap, inBackpacks, backpack) local amount = amount or 1 local subType = subType or 0 + local startingAmount = amount + local result = RETURNVALUE_NOERROR + local bpSize = 20 + local itemType = ItemType(itemid) - if ItemType(itemid):isStackable() then + if itemType:isStackable() then local stuff + local bpsToAdd = math.ceil(amount / itemType:getStackSize() / bpSize) if inBackpacks then - stuff = Game.createItem(backpack, 1) - stuff:addItem(itemid, math.min(ITEM_STACK_SIZE, amount)) + local bps = {} + for i = 1, bpsToAdd, 1 do + stuff = Game.createItem(backpack, 1) + local itemAdded = true + while startingAmount > 0 and itemAdded do + local item = stuff:addItem(itemid, math.min(itemType:getStackSize(), startingAmount)) + if item then + startingAmount = startingAmount - math.min(itemType:getStackSize(), startingAmount) + itemAdded = true + else + itemAdded = false + end + end + bps[i] = stuff + end + for i = 1, bpsToAdd, 1 do + result = result and Player(cid):addItemEx(bps[i], ignoreCap) + end + + if result ~= RETURNVALUE_NOERROR then + for i = 1, bpsToAdd, 1 do + if bps[i] then + bps[i]:remove() + end + end + end else - stuff = Game.createItem(itemid, math.min(ITEM_STACK_SIZE, amount)) + while startingAmount > 0 and result == RETURNVALUE_NOERROR do + stuff = Game.createItem(itemid, math.min(itemType:getStackSize(), startingAmount)) + if result == RETURNVALUE_NOERROR then + result = Player(cid):addItemEx(stuff, ignoreCap) + startingAmount = startingAmount - math.min(itemType:getStackSize(), startingAmount) + end + end end - return Player(cid):addItemEx(stuff, ignoreCap) ~= RETURNVALUE_NOERROR and 0 or amount, 0 + + return result ~= RETURNVALUE_NOERROR and 0 or amount - startingAmount, 0 end local a = 0 diff --git a/src/container.cpp b/src/container.cpp index ec11f4a953..11822136e9 100644 --- a/src/container.cpp +++ b/src/container.cpp @@ -347,22 +347,22 @@ ReturnValue Container::queryMaxCount(int32_t index, const Thing& thing, uint32_t uint32_t slotIndex = 0; for (Item* containerItem : itemlist) { if (containerItem != item && containerItem->equals(item) && - containerItem->getItemCount() < ITEM_STACK_SIZE) { + containerItem->getItemCount() < containerItem->getStackSize()) { if (queryAdd(slotIndex++, *item, count, flags) == RETURNVALUE_NOERROR) { - n += ITEM_STACK_SIZE - containerItem->getItemCount(); + n += containerItem->getStackSize() - containerItem->getItemCount(); } } } } else { const Item* destItem = getItemByIndex(index); - if (item->equals(destItem) && destItem->getItemCount() < ITEM_STACK_SIZE) { + if (item->equals(destItem) && destItem->getItemCount() < destItem->getStackSize()) { if (queryAdd(index, *item, count, flags) == RETURNVALUE_NOERROR) { - n = ITEM_STACK_SIZE - destItem->getItemCount(); + n = destItem->getStackSize() - destItem->getItemCount(); } } } - maxQueryCount = freeSlots * ITEM_STACK_SIZE + n; + maxQueryCount = freeSlots * item->getStackSize() + n; if (maxQueryCount < count) { return RETURNVALUE_CONTAINERNOTENOUGHROOM; } @@ -462,14 +462,14 @@ Cylinder* Container::queryDestination(int32_t& index, const Thing& thing, Item** bool autoStack = !hasBitSet(FLAG_IGNOREAUTOSTACK, flags); if (autoStack && item->isStackable() && item->getParent() != this) { - if (*destItem && (*destItem)->equals(item) && (*destItem)->getItemCount() < ITEM_STACK_SIZE) { + if (*destItem && (*destItem)->equals(item) && (*destItem)->getItemCount() < (*destItem)->getStackSize()) { return this; } // try find a suitable item to stack with uint32_t n = 0; for (Item* listItem : itemlist) { - if (listItem != item && listItem->equals(item) && listItem->getItemCount() < ITEM_STACK_SIZE) { + if (listItem != item && listItem->equals(item) && listItem->getItemCount() < item->getStackSize()) { *destItem = listItem; index = n; return this; diff --git a/src/game.cpp b/src/game.cpp index 41ae78ddf3..f6471b3ebf 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1205,7 +1205,7 @@ ReturnValue Game::internalMoveItem(Cylinder* fromCylinder, Cylinder* toCylinder, uint32_t n; if (item->equals(toItem)) { - n = std::min(ITEM_STACK_SIZE - toItem->getItemCount(), m); + n = std::min(item->getStackSize() - toItem->getItemCount(), m); toCylinder->updateThing(toItem, toItem->getID(), toItem->getItemCount() + n); updateItem = toItem; } else { @@ -1329,7 +1329,7 @@ ReturnValue Game::internalAddItem(Cylinder* toCylinder, Item* item, int32_t inde if (item->isStackable() && item->equals(toItem)) { uint32_t m = std::min(item->getItemCount(), maxQueryCount); - uint32_t n = std::min(ITEM_STACK_SIZE - toItem->getItemCount(), m); + uint32_t n = std::min(item->getStackSize() - toItem->getItemCount(), m); toCylinder->updateThing(toItem, toItem->getID(), toItem->getItemCount() + n); @@ -1593,7 +1593,7 @@ void Game::addMoney(Cylinder* cylinder, uint64_t money, uint32_t flags /*= 0*/) money -= currencyCoins * worth; while (currencyCoins > 0) { - const uint16_t count = std::min(ITEM_STACK_SIZE, currencyCoins); + const uint16_t count = std::min(Item::items[it.second].stackSize, currencyCoins); Item* remaindItem = Item::CreateItem(it.second, count); @@ -1850,7 +1850,7 @@ void Game::playerEquipItem(uint32_t playerId, uint16_t spriteId) } if (slotItem && slotItem->getID() == it.id && - (!it.stackable || slotItem->getItemCount() == ITEM_STACK_SIZE || !equipItem)) { + (!it.stackable || slotItem->getItemCount() == slotItem->getStackSize() || !equipItem)) { internalMoveItem(slotItem->getParent(), player, CONST_SLOT_WHEREEVER, slotItem, slotItem->getItemCount(), nullptr, 0, player, nullptr, &fromPos, &toPos); } else if (equipItem) { @@ -2734,8 +2734,9 @@ void Game::playerRequestTrade(uint32_t playerId, const Position& pos, uint8_t st } Container* tradeContainer = tradeItem->getContainer(); - if (tradeContainer && tradeContainer->getItemHoldingCount() + 1 > ITEM_STACK_SIZE) { - player->sendCancelMessage("You can only trade up to " + std::to_string(ITEM_STACK_SIZE) + " objects at once."); + if (tradeContainer && tradeContainer->getItemHoldingCount() + 1 > tradeContainer->getStackSize()) { + player->sendCancelMessage("You can only trade up to " + std::to_string(tradeContainer->getStackSize()) + + " objects at once."); return; } @@ -3035,7 +3036,12 @@ void Game::internalCloseTrade(Player* player, bool sendCancel /* = true*/) void Game::playerPurchaseItem(uint32_t playerId, uint16_t spriteId, uint8_t count, uint16_t amount, bool ignoreCap /* = false*/, bool inBackpacks /* = false*/) { - if (amount == 0 || amount > ITEM_STACK_SIZE) { + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + if (amount == 0) { return; } @@ -3051,11 +3057,6 @@ void Game::playerPurchaseItem(uint32_t playerId, uint16_t spriteId, uint8_t coun return; } - const ItemType& it = Item::items.getItemIdByClientId(spriteId); - if (it.id == 0) { - return; - } - uint8_t subType; if (it.isSplash() || it.isFluidContainer()) { subType = clientFluidToServer(count); @@ -3072,7 +3073,12 @@ void Game::playerPurchaseItem(uint32_t playerId, uint16_t spriteId, uint8_t coun void Game::playerSellItem(uint32_t playerId, uint16_t spriteId, uint8_t count, uint16_t amount, bool ignoreEquipped) { - if (amount == 0 || amount > ITEM_STACK_SIZE) { + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + if (amount == 0) { return; } @@ -3088,11 +3094,6 @@ void Game::playerSellItem(uint32_t playerId, uint16_t spriteId, uint8_t count, u return; } - const ItemType& it = Item::items.getItemIdByClientId(spriteId); - if (it.id == 0) { - return; - } - uint8_t subType; if (it.isSplash() || it.isFluidContainer()) { subType = clientFluidToServer(count); @@ -5316,7 +5317,7 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.stackable) { uint16_t tmpAmount = offer.amount; while (tmpAmount > 0) { - int32_t stackCount = std::min(ITEM_STACK_SIZE, tmpAmount); + int32_t stackCount = std::min(it.stackSize, tmpAmount); Item* item = Item::CreateItem(it.id, stackCount); if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { delete item; @@ -5424,7 +5425,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { - uint16_t stackCount = std::min(ITEM_STACK_SIZE, tmpAmount); + uint16_t stackCount = std::min(it.stackSize, tmpAmount); Item* item = Item::CreateItem(it.id, stackCount); if (internalAddItem(buyerPlayer->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { @@ -5471,7 +5472,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { - uint16_t stackCount = std::min(ITEM_STACK_SIZE, tmpAmount); + uint16_t stackCount = std::min(it.stackSize, tmpAmount); Item* item = Item::CreateItem(it.id, stackCount); if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { delete item; diff --git a/src/game.h b/src/game.h index 40881a29a1..7380af1ffa 100644 --- a/src/game.h +++ b/src/game.h @@ -60,8 +60,6 @@ static constexpr int32_t RANGE_REQUEST_TRADE_INTERVAL = 400; static constexpr int32_t MAX_STACKPOS = 10; -static constexpr uint8_t ITEM_STACK_SIZE = 100; - /** * Main Game class. * This class is responsible to control everything that happens diff --git a/src/iomarket.cpp b/src/iomarket.cpp index 79cb5fb5f0..37f51700ce 100644 --- a/src/iomarket.cpp +++ b/src/iomarket.cpp @@ -130,7 +130,7 @@ void IOMarket::processExpiredOffers(DBResult_ptr result, bool) if (itemType.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { - uint16_t stackCount = std::min(ITEM_STACK_SIZE, tmpAmount); + uint16_t stackCount = std::min(itemType.stackSize, tmpAmount); Item* item = Item::CreateItem(itemType.id, stackCount); if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { diff --git a/src/item.h b/src/item.h index 0dafd089ec..bd37888d1b 100644 --- a/src/item.h +++ b/src/item.h @@ -897,6 +897,8 @@ class Item : virtual public Thing bool hasMarketAttributes() const; + uint8_t getStackSize() const { return items[id].stackSize; } + std::unique_ptr& getAttributes() { if (!attributes) { diff --git a/src/items.cpp b/src/items.cpp index d037dabcd5..c456e8a2d7 100644 --- a/src/items.cpp +++ b/src/items.cpp @@ -198,6 +198,7 @@ const std::unordered_map ItemParseAttributes {"storeitem", ITEM_PARSE_STOREITEM}, {"worth", ITEM_PARSE_WORTH}, {"supply", ITEM_PARSE_SUPPLY}, + {"stacksize", ITEM_PARSE_STACKSIZE}, }; const std::unordered_map ItemTypesMap = {{"key", ITEM_TYPE_KEY}, @@ -1865,6 +1866,11 @@ void Items::parseItemNode(const pugi::xml_node& itemNode, uint16_t id) break; } + case ITEM_PARSE_STACKSIZE: { + it.stackSize = pugi::cast(valueAttribute.value()); + break; + } + default: { // It should not ever get to here, only if you add a new key to the map and don't configure a case // for it. diff --git a/src/items.h b/src/items.h index 73f6054bff..c7931faefb 100644 --- a/src/items.h +++ b/src/items.h @@ -9,6 +9,8 @@ #include "itemloader.h" #include "position.h" +static constexpr uint8_t ITEM_STACK_SIZE = 100; + class ConditionDamage; enum SlotPositionBits : uint32_t @@ -172,6 +174,7 @@ enum ItemParseAttributes_t ITEM_PARSE_ALLOWDISTREAD, ITEM_PARSE_STOREITEM, ITEM_PARSE_WORTH, + ITEM_PARSE_STACKSIZE, ITEM_PARSE_REFLECTPERCENTALL, ITEM_PARSE_REFLECTPERCENTELEMENTS, ITEM_PARSE_REFLECTPERCENTMAGIC, @@ -353,6 +356,7 @@ class ItemType int32_t runeMagLevel = 0; int32_t runeLevel = 0; uint64_t worth = 0; + uint8_t stackSize = ITEM_STACK_SIZE; CombatType_t combatType = COMBAT_NONE; diff --git a/src/luascript.cpp b/src/luascript.cpp index 90a8a340f3..7a97d7bb64 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -3180,6 +3180,7 @@ void LuaScriptInterface::registerFunctions() registerMethod(L, "ItemType", "getCapacity", LuaScriptInterface::luaItemTypeGetCapacity); registerMethod(L, "ItemType", "getWeight", LuaScriptInterface::luaItemTypeGetWeight); registerMethod(L, "ItemType", "getWorth", LuaScriptInterface::luaItemTypeGetWorth); + registerMethod(L, "ItemType", "getStackSize", LuaScriptInterface::luaItemTypeGetStackSize); registerMethod(L, "ItemType", "getHitChance", LuaScriptInterface::luaItemTypeGetHitChance); registerMethod(L, "ItemType", "getShootRange", LuaScriptInterface::luaItemTypeGetShootRange); @@ -3666,7 +3667,7 @@ int LuaScriptInterface::luaDoPlayerAddItem(lua_State* L) itemCount = std::max(1, count); } else if (it.hasSubType()) { if (it.stackable) { - itemCount = static_cast(std::ceil(static_cast(count) / ITEM_STACK_SIZE)); + itemCount = static_cast(std::ceil(static_cast(count) / it.stackSize)); } else { itemCount = 1; } @@ -3677,8 +3678,8 @@ int LuaScriptInterface::luaDoPlayerAddItem(lua_State* L) while (itemCount > 0) { uint16_t stackCount = subType; - if (it.stackable && stackCount > ITEM_STACK_SIZE) { - stackCount = ITEM_STACK_SIZE; + if (it.stackable && stackCount > it.stackSize) { + stackCount = it.stackSize; } Item* newItem = Item::CreateItem(itemId, stackCount); @@ -4962,7 +4963,7 @@ int LuaScriptInterface::luaGameCreateItem(lua_State* L) const ItemType& it = Item::items[id]; if (it.stackable) { - count = std::min(count, ITEM_STACK_SIZE); + count = std::min(count, it.stackSize); } Item* item = Item::CreateItem(id, count); @@ -5992,7 +5993,7 @@ int LuaScriptInterface::luaTileAddItem(lua_State* L) uint32_t subType = tfs::lua::getNumber(L, 3, 1); - Item* item = Item::CreateItem(itemId, std::min(subType, ITEM_STACK_SIZE)); + Item* item = Item::CreateItem(itemId, std::min(subType, Item::items[itemId].stackSize)); if (!item) { lua_pushnil(L); return 1; @@ -7263,7 +7264,7 @@ int LuaScriptInterface::luaItemTransform(lua_State* L) const ItemType& it = Item::items[itemId]; if (it.stackable) { - subType = std::min(subType, ITEM_STACK_SIZE); + subType = std::min(subType, it.stackSize); } ScriptEnvironment* env = tfs::lua::getScriptEnv(); @@ -7561,7 +7562,7 @@ int LuaScriptInterface::luaContainerAddItem(lua_State* L) if (it.hasSubType()) { if (it.stackable) { - itemCount = std::ceil(count / static_cast(ITEM_STACK_SIZE)); + itemCount = std::ceil(count / static_cast(it.stackSize)); } subType = count; @@ -7581,7 +7582,7 @@ int LuaScriptInterface::luaContainerAddItem(lua_State* L) uint32_t flags = tfs::lua::getNumber(L, 5, 0); for (int32_t i = 1; i <= itemCount; ++i) { - int32_t stackCount = std::min(subType, ITEM_STACK_SIZE); + int32_t stackCount = std::min(subType, it.stackSize); Item* item = Item::CreateItem(itemId, stackCount); if (!item) { reportErrorFunc(L, tfs::lua::getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); @@ -10098,7 +10099,7 @@ int LuaScriptInterface::luaPlayerAddItem(lua_State* L) itemCount = std::max(1, count); } else if (it.hasSubType()) { if (it.stackable) { - itemCount = std::ceil(count / static_cast(ITEM_STACK_SIZE)); + itemCount = std::ceil(count / static_cast(it.stackSize)); } subType = count; @@ -10119,7 +10120,7 @@ int LuaScriptInterface::luaPlayerAddItem(lua_State* L) for (int32_t i = 1; i <= itemCount; ++i) { int32_t stackCount = subType; if (it.stackable) { - stackCount = std::min(stackCount, ITEM_STACK_SIZE); + stackCount = std::min(stackCount, it.stackSize); subType -= stackCount; } @@ -13685,6 +13686,18 @@ int LuaScriptInterface::luaItemTypeGetWorth(lua_State* L) return 1; } +int LuaScriptInterface::luaItemTypeGetStackSize(lua_State* L) +{ + // itemType:getStackSize() + const ItemType* itemType = tfs::lua::getUserdata(L, 1); + if (!itemType) { + lua_pushnil(L); + return 1; + } + lua_pushnumber(L, itemType->stackSize); + return 1; +} + int LuaScriptInterface::luaItemTypeGetHitChance(lua_State* L) { // itemType:getHitChance() diff --git a/src/luascript.h b/src/luascript.h index fa067cc988..7c18a87235 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -1017,6 +1017,7 @@ class LuaScriptInterface static int luaItemTypeGetCapacity(lua_State* L); static int luaItemTypeGetWeight(lua_State* L); static int luaItemTypeGetWorth(lua_State* L); + static int luaItemTypeGetStackSize(lua_State* L); static int luaItemTypeGetHitChance(lua_State* L); static int luaItemTypeGetShootRange(lua_State* L); diff --git a/src/npc.cpp b/src/npc.cpp index add4658be9..4273b3db6c 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -1124,7 +1124,7 @@ int NpcScriptInterface::luaDoSellItem(lua_State* L) const ItemType& it = Item::items[itemId]; if (it.stackable) { while (amount > 0) { - int32_t stackCount = std::min(ITEM_STACK_SIZE, amount); + int32_t stackCount = std::min(it.stackSize, amount); Item* item = Item::CreateItem(it.id, stackCount); if (item && actionId != 0) { item->setActionId(actionId); diff --git a/src/player.cpp b/src/player.cpp index c7e4b92dbb..0bbf7b229e 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2677,7 +2677,7 @@ ReturnValue Player::queryMaxCount(int32_t index, const Thing& thing, uint32_t co } } } else if (inventoryItem->isStackable() && item->equals(inventoryItem) && - inventoryItem->getItemCount() < ITEM_STACK_SIZE) { + inventoryItem->getItemCount() < inventoryItem->getStackSize()) { uint32_t remainder = (100 - inventoryItem->getItemCount()); if (queryAdd(slotIndex, *item, remainder, flags) == RETURNVALUE_NOERROR) { @@ -2686,7 +2686,7 @@ ReturnValue Player::queryMaxCount(int32_t index, const Thing& thing, uint32_t co } } else if (queryAdd(slotIndex, *item, item->getItemCount(), flags) == RETURNVALUE_NOERROR) { // empty slot if (item->isStackable()) { - n += 100; + n += item->getStackSize(); } else { ++n; } @@ -2703,14 +2703,15 @@ ReturnValue Player::queryMaxCount(int32_t index, const Thing& thing, uint32_t co } if (destItem) { - if (destItem->isStackable() && item->equals(destItem) && destItem->getItemCount() < ITEM_STACK_SIZE) { - maxQueryCount = 100 - destItem->getItemCount(); + if (destItem->isStackable() && item->equals(destItem) && + destItem->getItemCount() < destItem->getStackSize()) { + maxQueryCount = destItem->getStackSize() - destItem->getItemCount(); } else { maxQueryCount = 0; } } else if (queryAdd(index, *item, count, flags) == RETURNVALUE_NOERROR) { // empty slot if (item->isStackable()) { - maxQueryCount = 100; + maxQueryCount = item->getStackSize(); } else { maxQueryCount = 1; } @@ -2777,7 +2778,8 @@ Cylinder* Player::queryDestination(int32_t& index, const Thing& thing, Item** de if (autoStack && isStackable) { // try find an already existing item to stack with if (queryAdd(slotIndex, *item, item->getItemCount(), 0) == RETURNVALUE_NOERROR) { - if (inventoryItem->equals(item) && inventoryItem->getItemCount() < ITEM_STACK_SIZE) { + if (inventoryItem->equals(item) && + inventoryItem->getItemCount() < inventoryItem->getStackSize()) { index = slotIndex; *destItem = inventoryItem; return this; @@ -2836,7 +2838,7 @@ Cylinder* Player::queryDestination(int32_t& index, const Thing& thing, Item** de } // try find an already existing item to stack with - if (tmpItem->equals(item) && tmpItem->getItemCount() < ITEM_STACK_SIZE) { + if (tmpItem->equals(item) && tmpItem->getItemCount() < tmpItem->getStackSize()) { index = n; *destItem = tmpItem; return tmpContainer;