Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stash support feature #4885

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions data/migrations/37.lua
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions data/migrations/38.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function onUpdateDatabase()
return false
end
8 changes: 8 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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", "");
Expand Down Expand Up @@ -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()) {
Expand Down
2 changes: 2 additions & 0 deletions src/configmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
};
Expand Down Expand Up @@ -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 */
};
Expand Down
8 changes: 8 additions & 0 deletions src/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
40 changes: 40 additions & 0 deletions src/container.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t> 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<Item*, uint32_t>(containerItem, static_cast<uint32_t>(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*, uint32_t>(item, static_cast<uint32_t>(item->getItemCount())));
}
}
}
return toReturnList;
}

Attr_ReadValue Container::readAttr(AttrTypes_t attr, PropStream& propStream)
{
if (attr == ATTR_CONTAINER_ITEMS) {
Expand Down Expand Up @@ -151,6 +181,16 @@ void Container::updateItemWeight(int32_t diff)
}
}

uint16_t Container::getFreeSlots() {
uint16_t counter = std::max<uint16_t>(0, capacity() - size());
for (auto item : itemlist) {
if (Container* container = item->getContainer()) {
counter += std::max<uint16_t>(0, container->getFreeSlots());
}
}
return counter;
}

uint32_t Container::getWeight() const { return Item::getWeight() + totalWeight; }

Item* Container::getItemByIndex(size_t index) const
Expand Down
2 changes: 2 additions & 0 deletions src/container.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
2 changes: 2 additions & 0 deletions src/enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,8 @@ using MarketOfferList = std::list<MarketOffer>;
using HistoryMarketOfferList = std::list<HistoryMarketOffer>;
using ShopInfoList = std::list<ShopInfo>;

using StashItemList = std::map<uint16_t, uint32_t>; // itemID, count

enum MonstersEvent_t : uint8_t
{
MONSTERS_EVENT_NONE = 0,
Expand Down
116 changes: 116 additions & 0 deletions src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions src/iologindata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t>("item_id");
uint32_t itemCount = result->getNumber<uint32_t>("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(
Expand Down Expand Up @@ -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()))) {
Expand Down
1 change: 1 addition & 0 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -943,5 +943,6 @@ class Item : virtual public Thing

using ItemList = std::list<Item*>;
using ItemDeque = std::deque<Item*>;
using StashContainerList = std::vector<std::pair<Item*, uint32_t>>;

#endif // FS_ITEM_H
Loading