From e831961829e5e8bd9764708b4fcc307878ad918a Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 1 Nov 2024 19:32:52 -0300 Subject: [PATCH] perf: improve players saving --- schema.sql | 3 +- src/creatures/CMakeLists.txt | 3 + .../components/player_forge_history.cpp | 110 +++ .../components/player_forge_history.hpp | 57 ++ .../players/components/player_stash.cpp | 141 ++++ .../players/components/player_stash.hpp | 40 + .../players/components/player_storage.cpp | 181 ++++ .../players/components/player_storage.hpp | 36 + .../players/imbuements/imbuements.cpp | 3 +- src/creatures/players/player.cpp | 149 ++-- src/creatures/players/player.hpp | 60 +- src/creatures/players/wheel/player_wheel.cpp | 5 +- src/database/database.cpp | 5 + src/database/database.hpp | 3 + src/game/scheduling/save_manager.cpp | 138 +-- src/game/scheduling/save_manager.hpp | 19 + src/io/functions/iologindata_load_player.cpp | 53 -- src/io/functions/iologindata_load_player.hpp | 3 - src/io/functions/iologindata_save_player.cpp | 787 ++++++++---------- src/io/functions/iologindata_save_player.hpp | 3 - src/io/io_bosstiary.cpp | 5 +- src/io/iologindata.cpp | 15 +- .../creatures/player/player_functions.cpp | 9 +- src/server/network/protocol/protocolgame.cpp | 6 +- 24 files changed, 1136 insertions(+), 698 deletions(-) create mode 100644 src/creatures/players/components/player_forge_history.cpp create mode 100644 src/creatures/players/components/player_forge_history.hpp create mode 100644 src/creatures/players/components/player_stash.cpp create mode 100644 src/creatures/players/components/player_stash.hpp create mode 100644 src/creatures/players/components/player_storage.cpp create mode 100644 src/creatures/players/components/player_storage.hpp diff --git a/schema.sql b/schema.sql index af245067057..d81c2914156 100644 --- a/schema.sql +++ b/schema.sql @@ -732,7 +732,8 @@ CREATE TABLE IF NOT EXISTS `player_bosstiary` ( `bossIdSlotOne` int NOT NULL DEFAULT 0, `bossIdSlotTwo` int NOT NULL DEFAULT 0, `removeTimes` int NOT NULL DEFAULT 1, - `tracker` blob NOT NULL + `tracker` blob NOT NULL, + PRIMARY KEY (`player_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `player_rewards` diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index c87a45a0aa0..8606d17796e 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -12,6 +12,9 @@ target_sources(${PROJECT_NAME}_lib PRIVATE npcs/npc.cpp npcs/npcs.cpp npcs/spawns/spawn_npc.cpp + players/components/player_forge_history.cpp + players/components/player_storage.cpp + players/components/player_stash.cpp players/grouping/familiars.cpp players/grouping/groups.cpp players/grouping/guild.cpp diff --git a/src/creatures/players/components/player_forge_history.cpp b/src/creatures/players/components/player_forge_history.cpp new file mode 100644 index 00000000000..9089731bbf5 --- /dev/null +++ b/src/creatures/players/components/player_forge_history.cpp @@ -0,0 +1,110 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#include "creatures/players/components/player_forge_history.hpp" + +#include "database/database.hpp" +#include "creatures/players/player.hpp" +#include "utils/tools.hpp" +#include "game/scheduling/save_manager.hpp" + +PlayerForgeHistory::PlayerForgeHistory(Player &player) : + m_player(player) { } + +const std::vector &PlayerForgeHistory::get() const { + return m_history; +} + +void PlayerForgeHistory::add(const ForgeHistory &history) { + m_history.push_back(history); + m_modifiedHistory.push_back(history); +} + +void PlayerForgeHistory::remove(int historyId) { + m_removedHistoryIds.push_back(historyId); + m_history.erase( + std::remove_if(m_history.begin(), m_history.end(), [historyId](const ForgeHistory &h) { return h.id == historyId; }), + m_history.end() + ); +} + +bool PlayerForgeHistory::load() { + auto playerGUID = m_player.getGUID(); + Database &db = Database::getInstance(); + auto query = fmt::format("SELECT * FROM forge_history WHERE player_id = {}", playerGUID); + const DBResult_ptr &result = db.storeQuery(query); + if (!result) { + g_logger().debug("Failed to load forge history for player with ID: {}", playerGUID); + return false; + } + + do { + ForgeHistory history; + history.id = result->getNumber("id"); + history.actionType = static_cast(result->getNumber("action_type")); + history.description = result->getString("description"); + history.createdAt = result->getNumber("done_at"); + history.success = result->getNumber("is_success"); + m_history.push_back(history); + } while (result->next()); + + return true; +} + +bool PlayerForgeHistory::save() { + if (m_modifiedHistory.empty() && m_removedHistoryIds.empty()) { + return true; + } + + auto playerGUID = m_player.getGUID(); + auto removedHistoryIds = m_removedHistoryIds; + auto modifiedHistory = m_modifiedHistory; + + auto forgeHistorySaveTask = [playerGUID, removedHistoryIds, modifiedHistory]() mutable { + Database &db = Database::getInstance(); + + if (!removedHistoryIds.empty()) { + std::string idsToDelete = fmt::format("{}", fmt::join(removedHistoryIds, ", ")); + std::string deleteQuery = fmt::format( + "DELETE FROM `forge_history` WHERE `player_id` = {} AND `id` IN ({})", + playerGUID, idsToDelete + ); + + if (!db.executeQuery(deleteQuery)) { + g_logger().error("Failed to delete forge history entries for player with ID: {}", playerGUID); + return; + } + + removedHistoryIds.clear(); + } + + DBInsert insertQuery("INSERT INTO `forge_history` (`id`, `player_id`, `action_type`, `description`, `done_at`, `is_success`) VALUES "); + insertQuery.upsert({ "action_type", "description", "done_at", "is_success" }); + + for (const auto &history : modifiedHistory) { + auto row = fmt::format("{}, {}, {}, {}, {}, {}", history.id, playerGUID, history.actionType, db.escapeString(history.description), history.createdAt, history.success ? 1 : 0); + + if (!insertQuery.addRow(row)) { + g_logger().warn("Failed to add forge history entry for player with ID: {}", playerGUID); + return; + } + + g_logger().debug("Added forge history entry date: {}, for player with ID: {}", formatDate(history.createdAt / 1000), playerGUID); + } + + if (!insertQuery.execute()) { + g_logger().error("Failed to execute insertion for forge history entries for player with ID: {}", playerGUID); + return; + } + }; + + g_saveManager().addTask(forgeHistorySaveTask, "PlayerForgeHistory::save - forgeHistorySaveTask"); + m_modifiedHistory.clear(); + return true; +} diff --git a/src/creatures/players/components/player_forge_history.hpp b/src/creatures/players/components/player_forge_history.hpp new file mode 100644 index 00000000000..9f0086dd41c --- /dev/null +++ b/src/creatures/players/components/player_forge_history.hpp @@ -0,0 +1,57 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#pragma once + +#include "enums/forge_conversion.hpp" + +class Player; + +struct ForgeHistory { + ForgeAction_t actionType = ForgeAction_t::FUSION; + uint8_t tier = 0; + uint8_t bonus = 0; + + uint64_t createdAt; + + uint16_t id = 0; + + uint64_t cost = 0; + uint64_t dustCost = 0; + uint64_t coresCost = 0; + uint64_t gained = 0; + + bool success = false; + bool tierLoss = false; + bool successCore = false; + bool tierCore = false; + bool convergence = false; + + std::string description; + std::string firstItemName; + std::string secondItemName; +}; + +class PlayerForgeHistory { +public: + PlayerForgeHistory(Player &player); + + const std::vector &get() const; + void add(const ForgeHistory &history); + void remove(int historyId); + + bool load(); + bool save(); + +private: + std::vector m_history; + std::vector m_modifiedHistory; + std::vector m_removedHistoryIds; + Player &m_player; +}; diff --git a/src/creatures/players/components/player_stash.cpp b/src/creatures/players/components/player_stash.cpp new file mode 100644 index 00000000000..72ca8ecd11e --- /dev/null +++ b/src/creatures/players/components/player_stash.cpp @@ -0,0 +1,141 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#include "creatures/players/components/player_stash.hpp" + +#include "database/database.hpp" +#include "items/item.hpp" +#include "creatures/players/player.hpp" +#include "game/scheduling/save_manager.hpp" + +PlayerStash::PlayerStash(Player &player) : + m_player(player) { } + +void PlayerStash::add(uint16_t itemId, uint32_t amount) { + const auto it = m_stashItems.find(itemId); + if (it != m_stashItems.end()) { + m_stashItems[itemId] += amount; + } else { + m_stashItems[itemId] = amount; + } + + m_modifiedItems[itemId] = m_stashItems[itemId]; // Track added/modified items + m_removedItems.erase(itemId); // Remove from removed if it was there +} + +uint32_t PlayerStash::getCount(uint16_t itemId) const { + const auto it = m_stashItems.find(itemId); + if (it != m_stashItems.end()) { + return it->second; + } + return 0; +} + +bool PlayerStash::remove(uint16_t itemId, uint32_t amount) { + auto it = m_stashItems.find(itemId); + if (it != m_stashItems.end()) { + if (it->second > amount) { + m_stashItems[itemId] -= amount; + m_modifiedItems[itemId] = m_stashItems[itemId]; // Track modified item + } else if (it->second == amount) { + m_stashItems.erase(itemId); + m_removedItems.insert(itemId); // Track removed item + m_modifiedItems.erase(itemId); // Ensure it's not in added items + } else { + return false; + } + return true; + } + return false; +} + +bool PlayerStash::find(uint16_t itemId) const { + return m_stashItems.find(itemId) != m_stashItems.end(); +} + +const std::map &PlayerStash::getItems() const { + return m_stashItems; +} + +uint16_t PlayerStash::getSize() const { + uint16_t size = 0; + for (const auto &[itemId, itemCount] : m_stashItems) { + size += ceil(itemCount / static_cast(Item::items[itemId].stackSize)); + } + return size; +} + +bool PlayerStash::save() { + if (m_modifiedItems.empty() && m_removedItems.empty()) { + return true; + } + + const auto playerGUID = m_player.getGUID(); + auto removedItems = m_removedItems; + auto modifiedItems = m_modifiedItems; + + auto stashSaveTask = [playerGUID, removedItems, modifiedItems]() mutable { + Database &db = Database::getInstance(); + + if (!removedItems.empty()) { + std::string removedItemIds = fmt::format("{}", fmt::join(removedItems, ", ")); + std::string deleteQuery = fmt::format( + "DELETE FROM `player_stash` WHERE `player_id` = {} AND `item_id` IN ({})", + playerGUID, removedItemIds + ); + + if (!db.executeQuery(deleteQuery)) { + g_logger().error("[PlayerStash::save] - Failed to delete removed items for player: {}", playerGUID); + return; + } + + removedItems.clear(); + } + + DBInsert insertQuery("INSERT INTO `player_stash` (`player_id`, `item_id`, `item_count`) VALUES "); + insertQuery.upsert({ "item_count" }); + + for (const auto &[itemId, itemCount] : modifiedItems) { + auto row = fmt::format("{}, {}, {}", playerGUID, itemId, itemCount); + if (!insertQuery.addRow(row)) { + g_logger().warn("[PlayerStash::save] - Failed to add row for stash item: {}", itemId); + return; + } + } + + if (!insertQuery.execute()) { + g_logger().error("[PlayerStash::save] - Failed to execute insertion for modified stash items for player: {}", playerGUID); + return; + } + }; + + g_saveManager().addTask(stashSaveTask, "PlayerStash::save - stashSaveTask"); + m_modifiedItems.clear(); + + return true; +} + +bool PlayerStash::load() { + Database &db = Database::getInstance(); + auto query = fmt::format("SELECT `item_count`, `item_id` FROM `player_stash` WHERE `player_id` = {}", m_player.getGUID()); + const DBResult_ptr &result = db.storeQuery(query); + if (!result) { + g_logger().debug("[PlayerStash::load] - Failed to load stash items for player: {}", m_player.getName()); + return false; + } + + do { + int itemId = result->getNumber("item_id"); + int itemCount = result->getNumber("item_count"); + // The PlayerStash::add function should not be used, to avoid incrementing the add/modified maps + m_stashItems[itemId] = itemCount; + } while (result->next()); + + return true; +} diff --git a/src/creatures/players/components/player_stash.hpp b/src/creatures/players/components/player_stash.hpp new file mode 100644 index 00000000000..b3c83b248c4 --- /dev/null +++ b/src/creatures/players/components/player_stash.hpp @@ -0,0 +1,40 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#pragma once + +class Player; + +class PlayerStash { +public: + PlayerStash(Player &player); + + void add(uint16_t itemId, uint32_t count = 1); + + uint32_t getCount(uint16_t itemId) const; + + bool remove(uint16_t itemId, uint32_t amount); + + bool find(uint16_t itemId) const; + + const std::map &getItems() const; + + uint16_t getSize() const; + + bool load(); + + bool save(); + +private: + // Map of for the stash + std::map m_stashItems; + std::map m_modifiedItems; + std::set m_removedItems; + Player &m_player; +}; diff --git a/src/creatures/players/components/player_storage.cpp b/src/creatures/players/components/player_storage.cpp new file mode 100644 index 00000000000..2570abd5044 --- /dev/null +++ b/src/creatures/players/components/player_storage.cpp @@ -0,0 +1,181 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#include "creatures/players/components/player_storage.hpp" + +#include "database/database.hpp" +#include "game/scheduling/dispatcher.hpp" +#include "lua/callbacks/events_callbacks.hpp" +#include "lua/creature/events.hpp" +#include "creatures/appearance/outfit/outfit.hpp" +#include "creatures/players/grouping/familiars.hpp" +#include "creatures/players/player.hpp" +#include "creatures/players/storages/storages.hpp" +#include "game/scheduling/save_manager.hpp" + +PlayerStorage::PlayerStorage(Player &player) : + m_player(player) { } + +void PlayerStorage::add(const uint32_t key, const int32_t value, const bool shouldStorageUpdate /* = false*/, const bool shouldTrackModification /*= true*/) { + if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) { + if (IS_IN_KEYRANGE(key, OUTFITS_RANGE)) { + m_player.outfits.emplace_back( + value >> 16, + value & 0xFF + ); + return; + } + if (IS_IN_KEYRANGE(key, MOUNTS_RANGE)) { + // do nothing + } else if (IS_IN_KEYRANGE(key, FAMILIARS_RANGE)) { + m_player.familiars.emplace_back( + value >> 16 + ); + return; + } else { + g_logger().warn("Unknown reserved key: {} for player: {}", key, m_player.getName()); + return; + } + } + + if (value != -1) { + int32_t oldValue = get(key); + m_storageMap[key] = value; + + if (shouldTrackModification) { + m_modifiedKeys.insert(key); + } + if (!shouldStorageUpdate) { + auto currentFrameTime = g_dispatcher().getDispatcherCycle(); + const auto &player = m_player.getPlayer(); + g_events().eventOnStorageUpdate(player, key, value, oldValue, currentFrameTime); + g_callbacks().executeCallback(EventCallback_t::playerOnStorageUpdate, &EventCallback::playerOnStorageUpdate, player, key, value, oldValue, currentFrameTime); + } + } else { + m_storageMap.erase(key); + m_modifiedKeys.erase(key); + m_removedKeys.insert(key); + } +} + +int32_t PlayerStorage::get(const uint32_t key) const { + int32_t value = -1; + const auto it = m_storageMap.find(key); + if (it == m_storageMap.end()) { + return value; + } + + value = it->second; + return value; +} + +bool PlayerStorage::has(uint32_t key) const { + return m_storageMap.find(key) != m_storageMap.end(); +} + +int32_t PlayerStorage::get(const std::string &storageName) const { + const auto it = g_storages().getStorageMap().find(storageName); + if (it == g_storages().getStorageMap().end()) { + return -1; + } + const uint32_t key = it->second; + + return get(key); +} + +void PlayerStorage::add(const std::string &storageName, const int32_t value) { + auto it = g_storages().getStorageMap().find(storageName); + if (it == g_storages().getStorageMap().end()) { + g_logger().error("[{}] Storage name '{}' not found in storage map, register your storage in 'storages.xml' first for use", __func__, storageName); + return; + } + const uint32_t key = it->second; + add(key, value); +} + +bool PlayerStorage::save() { + if (m_removedKeys.empty() && m_modifiedKeys.empty()) { + return true; + } + + auto saveTask = [removedKeys = m_removedKeys, modifiedKeys = m_modifiedKeys, playerGUID = m_player.getGUID(), playerName = m_player.getName(), storageMap = m_storageMap]() mutable { + if (!removedKeys.empty()) { + std::string keysList = fmt::format("{}", fmt::join(removedKeys, ", ")); + std::string deleteQuery = fmt::format( + "DELETE FROM `player_storage` WHERE `player_id` = {} AND `key` IN ({})", + playerGUID, keysList + ); + + if (!g_database().executeQuery(deleteQuery)) { + g_logger().error("[SaveManager::playerStorageSaveTask] - Failed to delete storage keys for player: {}", playerName); + return; + } + removedKeys.clear(); + } + + if (!modifiedKeys.empty()) { + DBInsert storageQuery("INSERT INTO `player_storage` (`player_id`, `key`, `value`) VALUES "); + storageQuery.upsert({ "value" }); + + for (const auto &key : modifiedKeys) { + auto row = fmt::format("{}, {}, {}", playerGUID, key, storageMap.at(key)); + if (!storageQuery.addRow(row)) { + g_logger().warn("[SaveManager::playerStorageSaveTask] - Failed to add row for player storage: {}", playerName); + return; + } + } + + if (!storageQuery.execute()) { + g_logger().error("[SaveManager::playerStorageSaveTask] - Failed to execute storage insertion for player: {}", playerName); + return; + } + + modifiedKeys.clear(); + } + }; + + g_saveManager().addTask(saveTask, "PlayerStorage::save - playerStorageSaveTask"); + return true; +} + +bool PlayerStorage::load() { + Database &db = Database::getInstance(); + auto query = fmt::format("SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = {}", m_player.getGUID()); + const DBResult_ptr &result = db.storeQuery(query); + if (!result) { + g_logger().debug("[PlayerStorage::load] - Failed to load storage keys for player: {}", m_player.getName()); + return false; + } + + do { + uint32_t key = result->getNumber("key"); + int32_t value = result->getNumber("value"); + add(key, value, true, false); + } while (result->next()); + + return true; +} + +void PlayerStorage::getReservedRange() { + // Generate outfits range + uint32_t outfits_key = PSTRG_OUTFITS_RANGE_START; + for (const auto &entry : m_player.outfits) { + uint32_t key = ++outfits_key; + m_storageMap[key] = (entry.lookType << 16) | entry.addons; + m_modifiedKeys.insert(key); // Track the key for saving + } + + // Generate familiars range + uint32_t familiar_key = PSTRG_FAMILIARS_RANGE_START; + for (const auto &entry : m_player.familiars) { + uint32_t key = ++familiar_key; + m_storageMap[key] = (entry.lookType << 16); + m_modifiedKeys.insert(key); // Track the key for saving + } +} diff --git a/src/creatures/players/components/player_storage.hpp b/src/creatures/players/components/player_storage.hpp new file mode 100644 index 00000000000..d7344b017ec --- /dev/null +++ b/src/creatures/players/components/player_storage.hpp @@ -0,0 +1,36 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#pragma once + +class Player; + +class PlayerStorage { +public: + explicit PlayerStorage(Player &player); + + void add(const uint32_t key, const int32_t value, const bool shouldStorageUpdate = false, const bool shouldTrackModification = true); + bool remove(const uint32_t key); + bool has(const uint32_t key) const; + int32_t get(const uint32_t key) const; + + int32_t get(const std::string &storageName) const; + void add(const std::string &storageName, const int32_t value); + + bool save(); + bool load(); + +private: + void getReservedRange(); + + Player &m_player; + std::map m_storageMap; + std::set m_modifiedKeys; + std::set m_removedKeys; +}; diff --git a/src/creatures/players/imbuements/imbuements.cpp b/src/creatures/players/imbuements/imbuements.cpp index c71a56fd588..fc6867d4c27 100644 --- a/src/creatures/players/imbuements/imbuements.cpp +++ b/src/creatures/players/imbuements/imbuements.cpp @@ -11,6 +11,7 @@ #include "config/configmanager.hpp" #include "creatures/players/player.hpp" +#include "creatures/players/components/player_storage.hpp" #include "items/item.hpp" #include "lib/di/container.hpp" #include "utils/pugicast.hpp" @@ -356,7 +357,7 @@ std::vector Imbuements::getImbuements(const std::shared_ptr // Parse the storages for each imbuement in imbuements.xml and config.lua (enable/disable storage) if (g_configManager().getBoolean(TOGGLE_IMBUEMENT_SHRINE_STORAGE) && imbuement->getStorage() != 0 - && player->getStorageValue(imbuement->getStorage() == -1) + && player->storage()->get(imbuement->getStorage() == -1) && imbuement->getBaseID() >= 1 && imbuement->getBaseID() <= 3) { continue; } diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 65ace44f5f6..f240390c247 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -18,6 +18,9 @@ #include "creatures/monsters/monster.hpp" #include "creatures/monsters/monsters.hpp" #include "creatures/npcs/npc.hpp" +#include "creatures/players/components/player_forge_history.hpp" +#include "creatures/players/components/player_stash.hpp" +#include "creatures/players/components/player_storage.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/wheel/wheel_gems.hpp" #include "creatures/players/achievement/player_achievement.hpp" @@ -75,6 +78,9 @@ Player::Player(std::shared_ptr p) : m_playerBadge = std::make_unique(*this); m_playerCyclopedia = std::make_unique(*this); m_playerTitle = std::make_unique(*this); + m_stashPlayer = std::make_unique(*this); + m_forgeHistoryPlayer = std::make_unique(*this); + m_storagePlayer = std::make_unique(*this); } Player::~Player() { @@ -1041,73 +1047,6 @@ uint16_t Player::getLookCorpse() const { return ITEM_MALE_CORPSE; } -void Player::addStorageValue(const uint32_t key, const int32_t value, const bool isLogin /* = false*/) { - if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) { - if (IS_IN_KEYRANGE(key, OUTFITS_RANGE)) { - outfits.emplace_back( - value >> 16, - value & 0xFF - ); - return; - } - if (IS_IN_KEYRANGE(key, MOUNTS_RANGE)) { - // do nothing - } else if (IS_IN_KEYRANGE(key, FAMILIARS_RANGE)) { - familiars.emplace_back( - value >> 16 - ); - return; - } else { - g_logger().warn("Unknown reserved key: {} for player: {}", key, getName()); - return; - } - } - - if (value != -1) { - int32_t oldValue = getStorageValue(key); - storageMap[key] = value; - - if (!isLogin) { - auto currentFrameTime = g_dispatcher().getDispatcherCycle(); - g_events().eventOnStorageUpdate(static_self_cast(), key, value, oldValue, currentFrameTime); - g_callbacks().executeCallback(EventCallback_t::playerOnStorageUpdate, &EventCallback::playerOnStorageUpdate, getPlayer(), key, value, oldValue, currentFrameTime); - } - } else { - storageMap.erase(key); - } -} - -int32_t Player::getStorageValue(const uint32_t key) const { - int32_t value = -1; - const auto it = storageMap.find(key); - if (it == storageMap.end()) { - return value; - } - - value = it->second; - return value; -} - -int32_t Player::getStorageValueByName(const std::string &storageName) const { - const auto it = g_storages().getStorageMap().find(storageName); - if (it == g_storages().getStorageMap().end()) { - return -1; - } - const uint32_t key = it->second; - - return getStorageValue(key); -} - -void Player::addStorageValueByName(const std::string &storageName, const int32_t value, const bool isLogin /* = false*/) { - auto it = g_storages().getStorageMap().find(storageName); - if (it == g_storages().getStorageMap().end()) { - g_logger().error("[{}] Storage name '{}' not found in storage map, register your storage in 'storages.xml' first for use", __func__, storageName); - return; - } - const uint32_t key = it->second; - addStorageValue(key, value, isLogin); -} - std::shared_ptr Player::kv() const { return g_kv().scoped("player")->scoped(fmt::format("{}", getGUID())); } @@ -4845,7 +4784,7 @@ void Player::setHazardSystemPoints(int32_t count) { if (!g_configManager().getBoolean(TOGGLE_HAZARDSYSTEM)) { return; } - addStorageValue(STORAGEVALUE_HAZARDCOUNT, std::max(0, std::min(0xFFFF, count)), true); + storage()->add(STORAGEVALUE_HAZARDCOUNT, std::max(0, std::min(0xFFFF, count)), true); reloadHazardSystemPointsCounter = true; if (count > 0) { setIcon("hazard", CreatureIcon(CreatureIconQuests_t::Hazard, count)); @@ -4855,7 +4794,7 @@ void Player::setHazardSystemPoints(int32_t count) { } uint16_t Player::getHazardSystemPoints() const { - const int32_t points = getStorageValue(STORAGEVALUE_HAZARDCOUNT); + const int32_t points = storage()->get(STORAGEVALUE_HAZARDCOUNT); if (points <= 0) { return 0; } @@ -5847,19 +5786,6 @@ bool Player::canWear(uint16_t lookType, uint8_t addons) const { return false; } -void Player::genReservedStorageRange() { - // generate outfits range - uint32_t outfits_key = PSTRG_OUTFITS_RANGE_START; - for (const auto &entry : outfits) { - storageMap[++outfits_key] = (entry.lookType << 16) | entry.addons; - } - // generate familiars range - uint32_t familiar_key = PSTRG_FAMILIARS_RANGE_START; - for (const auto &entry : familiars) { - storageMap[++familiar_key] = (entry.lookType << 16); - } -} - void Player::setSpecialMenuAvailable(bool supplyStashBool, bool marketMenuBool, bool depotSearchBool) { // Closing depot search when player have special container disabled and it's still open. if (isDepotSearchOpen() && !depotSearchBool && depotSearch) { @@ -6810,7 +6736,7 @@ void Player::sendUnjustifiedPoints() const { } uint8_t Player::getLastMount() const { - const int32_t value = getStorageValue(PSTRG_MOUNTS_CURRENTMOUNT); + const int32_t value = storage()->get(PSTRG_MOUNTS_CURRENTMOUNT); if (value > 0) { return value; } @@ -6818,7 +6744,7 @@ uint8_t Player::getLastMount() const { } uint8_t Player::getCurrentMount() const { - const int32_t value = getStorageValue(PSTRG_MOUNTS_CURRENTMOUNT); + const int32_t value = storage()->get(PSTRG_MOUNTS_CURRENTMOUNT); if (value > 0) { return value; } @@ -6826,7 +6752,7 @@ uint8_t Player::getCurrentMount() const { } void Player::setCurrentMount(uint8_t mount) { - addStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, mount); + storage()->add(PSTRG_MOUNTS_CURRENTMOUNT, mount); } bool Player::hasAnyMount() const { @@ -6936,14 +6862,14 @@ bool Player::tameMount(uint8_t mountId) { const uint8_t tmpMountId = mountId - 1; const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31); - int32_t value = getStorageValue(key); + int32_t value = storage()->get(key); if (value != -1) { value |= (1 << (tmpMountId % 31)); } else { value = (1 << (tmpMountId % 31)); } - addStorageValue(key, value); + storage()->add(key, value); return true; } @@ -6955,13 +6881,13 @@ bool Player::untameMount(uint8_t mountId) { const uint8_t tmpMountId = mountId - 1; const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31); - int32_t value = getStorageValue(key); + int32_t value = storage()->get(key); if (value == -1) { return true; } value &= ~(1 << (tmpMountId % 31)); - addStorageValue(key, value); + storage()->add(key, value); if (getCurrentMount() == mountId) { if (isMounted()) { @@ -6987,7 +6913,7 @@ bool Player::hasMount(const std::shared_ptr &mount) const { const uint8_t tmpMountId = mount->id - 1; - const int32_t value = getStorageValue(PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31)); + const int32_t value = storage()->get(PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31)); if (value == -1) { return false; } @@ -8698,7 +8624,7 @@ bool Player::saySpell(SpeakClasses type, const std::string &text, bool isGhostMo for (const auto &spectator : spectators) { if (const auto &tmpPlayer = spectator->getPlayer()) { if (g_configManager().getBoolean(EMOTE_SPELLS)) { - valueEmote = tmpPlayer->getStorageValue(STORAGEVALUE_EMOTE); + valueEmote = tmpPlayer->storage()->get(STORAGEVALUE_EMOTE); } if (!isGhostMode || tmpPlayer->canSeeCreature(static_self_cast())) { if (valueEmote == 1) { @@ -9366,14 +9292,6 @@ uint64_t Player::getForgeDustLevel() const { return forgeDustLevel; } -std::vector &Player::getForgeHistory() { - return forgeHistoryVector; -} - -void Player::setForgeHistory(const ForgeHistory &history) { - forgeHistoryVector.emplace_back(history); -} - void Player::registerForgeHistoryDescription(ForgeHistory history) { std::string successfulString = history.success ? "Successful" : "Unsuccessful"; std::string historyTierString = history.tier > 0 ? "tier - 1" : "consumed"; @@ -9524,7 +9442,7 @@ void Player::registerForgeHistoryDescription(ForgeHistory history) { history.description = detailsResponse.str(); - setForgeHistory(history); + forgeHistory()->add(history); } // Quickloot @@ -10247,6 +10165,33 @@ const std::unique_ptr &Player::cyclopedia() const { return m_playerCyclopedia; } +// Stash interface +std::unique_ptr &Player::stash() { + return m_stashPlayer; +} + +const std::unique_ptr &Player::stash() const { + return m_stashPlayer; +} + +// Forge history interface +std::unique_ptr &Player::forgeHistory() { + return m_forgeHistoryPlayer; +} + +const std::unique_ptr &Player::forgeHistory() const { + return m_forgeHistoryPlayer; +} + +// Storage interface +std::unique_ptr &Player::storage() { + return m_storagePlayer; +} + +const std::unique_ptr &Player::storage() const { + return m_storagePlayer; +} + void Player::sendLootMessage(const std::string &message) const { const auto &party = getParty(); if (!party) { @@ -10357,12 +10302,12 @@ void Player::BestiarysendCharms() const { void Player::addBestiaryKillCount(uint16_t raceid, uint32_t amount) { const uint32_t oldCount = getBestiaryKillCount(raceid); const uint32_t key = STORAGEVALUE_BESTIARYKILLCOUNT + raceid; - addStorageValue(key, static_cast(oldCount + amount), true); + storage()->add(key, static_cast(oldCount + amount), true); } uint32_t Player::getBestiaryKillCount(uint16_t raceid) const { const uint32_t key = STORAGEVALUE_BESTIARYKILLCOUNT + raceid; - const auto value = getStorageValue(key); + const auto value = storage()->get(key); return value > 0 ? static_cast(value) : 0; } diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 03f248cfa2f..c645925e081 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -48,6 +48,9 @@ class Container; class KV; class BedItem; class Npc; +class PlayerStorage; +class PlayerStash; +class PlayerForgeHistory; struct ModalWindow; struct Achievement; @@ -76,30 +79,7 @@ using ItemVector = std::vector>; using UsersMap = std::map>; using InvitedMap = std::map>; -struct ForgeHistory { - ForgeAction_t actionType = ForgeAction_t::FUSION; - uint8_t tier = 0; - uint8_t bonus = 0; - - time_t createdAt; - - uint16_t historyId = 0; - - uint64_t cost = 0; - uint64_t dustCost = 0; - uint64_t coresCost = 0; - uint64_t gained = 0; - - bool success = false; - bool tierLoss = false; - bool successCore = false; - bool tierCore = false; - bool convergence = false; - - std::string description; - std::string firstItemName; - std::string secondItemName; -}; +struct ForgeHistory; struct OpenContainer { std::shared_ptr container; @@ -366,16 +346,8 @@ class Player final : public Creature, public Cylinder, public Bankable { bool canOpenCorpse(uint32_t ownerId) const; - void addStorageValue(uint32_t key, int32_t value, bool isLogin = false); - int32_t getStorageValue(uint32_t key) const; - - int32_t getStorageValueByName(const std::string &storageName) const; - void addStorageValueByName(const std::string &storageName, int32_t value, bool isLogin = false); - std::shared_ptr kv() const; - void genReservedStorageRange(); - void setGroup(std::shared_ptr newGroup) { group = std::move(newGroup); } @@ -1150,10 +1122,6 @@ class Player final : public Creature, public Cylinder, public Bankable { void removeForgeDustLevel(uint64_t amount); uint64_t getForgeDustLevel() const; - std::vector &getForgeHistory(); - - void setForgeHistory(const ForgeHistory &history); - void registerForgeHistoryDescription(ForgeHistory history); void setBossPoints(uint32_t amount); @@ -1250,6 +1218,18 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unique_ptr &vip(); const std::unique_ptr &vip() const; + // Player stash methods interface + std::unique_ptr &stash(); + const std::unique_ptr &stash() const; + + // Player stash methods interface + std::unique_ptr &forgeHistory(); + const std::unique_ptr &forgeHistory() const; + + // Player storage methods interface + std::unique_ptr &storage(); + const std::unique_ptr &storage() const; + void sendLootMessage(const std::string &message) const; std::shared_ptr getLootPouch(); @@ -1341,13 +1321,11 @@ class Player final : public Creature, public Cylinder, public Bankable { std::map> depotLockerMap; std::map> depotChests; std::map moduleDelayMap; - std::map storageMap; std::map itemPriceMap; std::map> rewardMap; std::map, std::shared_ptr>> m_managedContainers; - std::vector forgeHistoryVector; std::vector quickLootListItemIds; @@ -1614,6 +1592,9 @@ class Player final : public Creature, public Cylinder, public Bankable { friend class PlayerCyclopedia; friend class PlayerTitle; friend class PlayerVIP; + friend class PlayerStorage; + friend class PlayerStash; + friend class PlayerForgeHistory; std::unique_ptr m_wheelPlayer; std::unique_ptr m_playerAchievement; @@ -1621,6 +1602,9 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unique_ptr m_playerCyclopedia; std::unique_ptr m_playerTitle; std::unique_ptr m_playerVIP; + std::unique_ptr m_stashPlayer; + std::unique_ptr m_forgeHistoryPlayer; + std::unique_ptr m_storagePlayer; std::mutex quickLootMutex; diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp index 6d3ee2713a7..f59e33bcc10 100644 --- a/src/creatures/players/wheel/player_wheel.cpp +++ b/src/creatures/players/wheel/player_wheel.cpp @@ -13,6 +13,7 @@ #include "creatures/combat/condition.hpp" #include "creatures/combat/spells.hpp" #include "creatures/players/player.hpp" +#include "creatures/players/components/player_storage.hpp" #include "creatures/players/vocations/vocation.hpp" #include "creatures/players/wheel/wheel_gems.hpp" #include "enums/player_wheel.hpp" @@ -3533,7 +3534,7 @@ uint8_t PlayerWheel::getGiftOfLifeValue() const { } int32_t PlayerWheel::getGiftOfCooldown() const { - const int32_t value = m_player.getStorageValue(STORAGEVALUE_GIFT_OF_LIFE_COOLDOWN_WOD); + const int32_t value = m_player.storage()->get(STORAGEVALUE_GIFT_OF_LIFE_COOLDOWN_WOD); if (value <= 0) { return 0; } @@ -3541,7 +3542,7 @@ int32_t PlayerWheel::getGiftOfCooldown() const { } void PlayerWheel::setGiftOfCooldown(int32_t value, bool isOnThink) { - m_player.addStorageValue(STORAGEVALUE_GIFT_OF_LIFE_COOLDOWN_WOD, value, true); + m_player.storage()->add(STORAGEVALUE_GIFT_OF_LIFE_COOLDOWN_WOD, value, true); if (!isOnThink) { setOnThinkTimer(WheelOnThink_t::GIFT_OF_LIFE, OTSYS_TIME() + 1000); } diff --git a/src/database/database.cpp b/src/database/database.cpp index fcb7371708d..61a325ee058 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -210,6 +210,7 @@ DBResult::DBResult(MYSQL_RES* res) { handle = res; int num_fields = mysql_num_fields(handle); + m_numFields = num_fields; const MYSQL_FIELD* fields = mysql_fetch_fields(handle); for (size_t i = 0; i < num_fields; i++) { @@ -275,6 +276,10 @@ size_t DBResult::countResults() const { return static_cast(mysql_num_rows(handle)); } +size_t DBResult::countColumns() const { + return m_numFields; +} + bool DBResult::hasNext() const { return row != nullptr; } diff --git a/src/database/database.hpp b/src/database/database.hpp index 69c47d324ad..8bd587716a7 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -164,6 +164,7 @@ class DBResult { static int8_t getInt8FromString(const std::string &string, const std::string &function); size_t countResults() const; + size_t countColumns() const; bool hasNext() const; bool next(); @@ -171,6 +172,8 @@ class DBResult { MYSQL_RES* handle; MYSQL_ROW row; + size_t m_numFields; + std::map listNames; friend class Database; diff --git a/src/game/scheduling/save_manager.cpp b/src/game/scheduling/save_manager.cpp index 9cccc4052a7..d0a88133d6b 100644 --- a/src/game/scheduling/save_manager.cpp +++ b/src/game/scheduling/save_manager.cpp @@ -16,9 +16,15 @@ #include "io/iologindata.hpp" #include "kv/kv.hpp" #include "lib/di/container.hpp" +#include "game/scheduling/task.hpp" SaveManager::SaveManager(ThreadPool &threadPool, KVStore &kvStore, Logger &logger, Game &game) : - threadPool(threadPool), kv(kvStore), logger(logger), game(game) { } + threadPool(threadPool), kv(kvStore), logger(logger), game(game) { + m_threads.reserve(threadPool.get_thread_count() + 1); + for (uint_fast16_t i = 0; i < m_threads.capacity(); ++i) { + m_threads.emplace_back(std::make_unique()); + } +} SaveManager &SaveManager::getInstance() { return inject(); @@ -27,40 +33,49 @@ SaveManager &SaveManager::getInstance() { void SaveManager::saveAll() { Benchmark bm_saveAll; logger.info("Saving server..."); - const auto players = game.getPlayers(); + Benchmark bm_players; + const auto &players = game.getPlayers(); + logger.info("Saving {} players...", players.size()); for (const auto &[_, player] : players) { player->loginPosition = player->getPosition(); doSavePlayer(player); } - auto guilds = game.getGuilds(); + double duration_players = bm_players.duration(); + if (duration_players > 1000.0) { + logger.info("Players saved in {:.2f} seconds.", duration_players / 1000.0); + } else { + logger.info("Players saved in {} milliseconds.", duration_players); + } + + Benchmark bm_guilds; + const auto &guilds = game.getGuilds(); for (const auto &[_, guild] : guilds) { saveGuild(guild); } + double duration_guilds = bm_guilds.duration(); + if (duration_guilds > 1000.0) { + logger.info("Guilds saved in {:.2f} seconds.", duration_guilds / 1000.0); + } else { + logger.info("Guilds saved in {} milliseconds.", duration_guilds); + } saveMap(); saveKV(); - logger.info("Server saved in {} milliseconds.", bm_saveAll.duration()); + + double duration_saveAll = bm_saveAll.duration(); + if (duration_saveAll > 1000.0) { + logger.info("Server saved in {:.2f} seconds.", duration_saveAll / 1000.0); + } else { + logger.info("Server saved in {} milliseconds.", duration_saveAll); + } } void SaveManager::scheduleAll() { auto scheduledAt = std::chrono::steady_clock::now(); m_scheduledAt = scheduledAt; - - // Disable save async if the config is set to false - if (!g_configManager().getBoolean(TOGGLE_SAVE_ASYNC)) { - saveAll(); - return; - } - - threadPool.detach_task([this, scheduledAt]() { - if (m_scheduledAt.load() != scheduledAt) { - logger.warn("Skipping save for server because another save has been scheduled."); - return; - } - saveAll(); - }); + saveAll(); } void SaveManager::schedulePlayer(std::weak_ptr playerPtr) { @@ -70,30 +85,7 @@ void SaveManager::schedulePlayer(std::weak_ptr playerPtr) { return; } - // Disable save async if the config is set to false - if (!g_configManager().getBoolean(TOGGLE_SAVE_ASYNC)) { - if (g_game().getGameState() == GAME_STATE_NORMAL) { - logger.debug("Saving player {}.", playerToSave->getName()); - } - doSavePlayer(playerToSave); - return; - } - - logger.debug("Scheduling player {} for saving.", playerToSave->getName()); - auto scheduledAt = std::chrono::steady_clock::now(); - m_playerMap[playerToSave->getGUID()] = scheduledAt; - threadPool.detach_task([this, playerPtr, scheduledAt]() { - auto player = playerPtr.lock(); - if (!player) { - logger.debug("Skipping save for player because player is no longer online."); - return; - } - if (m_playerMap[player->getGUID()] != scheduledAt) { - logger.warn("Skipping save for player because another save has been scheduled."); - return; - } - doSavePlayer(player); - }); + doSavePlayer(playerToSave); } bool SaveManager::doSavePlayer(std::shared_ptr player) { @@ -105,15 +97,11 @@ bool SaveManager::doSavePlayer(std::shared_ptr player) { Benchmark bm_savePlayer; Player::PlayerLock lock(player); m_playerMap.erase(player->getGUID()); - if (g_game().getGameState() == GAME_STATE_NORMAL) { - logger.debug("Saving player {}.", player->getName()); - } bool saveSuccess = IOLoginData::savePlayer(player); if (!saveSuccess) { logger.error("Failed to save player {}.", player->getName()); } - auto duration = bm_savePlayer.duration(); logger.debug("Saving player {} took {} milliseconds.", player->getName(), duration); return saveSuccess; @@ -122,6 +110,7 @@ bool SaveManager::doSavePlayer(std::shared_ptr player) { bool SaveManager::savePlayer(std::shared_ptr player) { if (player->isOnline()) { schedulePlayer(player); + logger.debug("saving player {}, but is online scheduling....", player->getName()); return true; } return doSavePlayer(player); @@ -132,35 +121,74 @@ void SaveManager::saveGuild(std::shared_ptr guild) { logger.debug("Failed to save guild because guild is null."); return; } - Benchmark bm_saveGuild; logger.debug("Saving guild {}...", guild->getName()); IOGuild::saveGuild(guild); - auto duration = bm_saveGuild.duration(); - logger.debug("Saving guild {} took {} milliseconds.", guild->getName(), duration); + if (duration > 100) { + logger.warn("Saving guild {} took {} milliseconds.", guild->getName(), duration); + } else { + logger.debug("Saving guild {} took {} milliseconds.", guild->getName(), duration); + } } void SaveManager::saveMap() { Benchmark bm_saveMap; - logger.debug("Saving map..."); + logger.info("Saving map..."); bool saveSuccess = Map::save(); if (!saveSuccess) { logger.error("Failed to save map."); } - auto duration = bm_saveMap.duration(); - logger.debug("Map saved in {} milliseconds.", duration); + double duration_map = bm_saveMap.duration(); + if (duration_map > 1000.0) { + logger.info("Map saved in {:.2f} seconds.", duration_map / 1000.0); + } else { + logger.info("Map saved in {} milliseconds.", duration_map); + } } void SaveManager::saveKV() { Benchmark bm_saveKV; - logger.debug("Saving key-value store..."); + logger.info("Saving key-value store..."); bool saveSuccess = kv.saveAll(); if (!saveSuccess) { logger.error("Failed to save key-value store."); } - auto duration = bm_saveKV.duration(); - logger.debug("Key-value store saved in {} milliseconds.", duration); + double duration_kv = bm_saveKV.duration(); + if (duration_kv > 1000.0) { + logger.info("KV store saved in {:.2f} seconds.", duration_kv / 1000.0); + } else { + logger.info("KV store saved in {} milliseconds.", duration_kv); + } +} + +void SaveManager::addTask(std::function &&f, std::string_view context, uint32_t expiresAfterMs /* = 0*/) { + const auto &thread = getThreadTask(); + std::scoped_lock lock(thread->mutex); + thread->tasks.emplace_back(expiresAfterMs, std::move(f), context); +} + +SaveManager::ThreadTask::ThreadTask() { + tasks.reserve(2000); +} + +void SaveManager::executeTasks() { + for (const auto &thread : m_threads) { + std::scoped_lock lock(thread->mutex); + auto &threadTasks = thread->tasks; + if (!threadTasks.empty()) { + m_tasks.insert(m_tasks.end(), make_move_iterator(thread->tasks.begin()), make_move_iterator(thread->tasks.end())); + threadTasks.clear(); + } + } + + threadPool.detach_task([tasks = std::move(m_tasks)] { + for (const auto &task : tasks) { + task.execute(); + } + }); + + m_tasks.clear(); } diff --git a/src/game/scheduling/save_manager.hpp b/src/game/scheduling/save_manager.hpp index 4bd66a7966f..13c4fb33a4d 100644 --- a/src/game/scheduling/save_manager.hpp +++ b/src/game/scheduling/save_manager.hpp @@ -16,6 +16,7 @@ class Logger; class Game; class Player; class Guild; +class Task; class SaveManager { public: @@ -32,7 +33,25 @@ class SaveManager { bool savePlayer(std::shared_ptr player); void saveGuild(std::shared_ptr guild); + void addTask(std::function &&f, std::string_view context, uint32_t expiresAfterMs = 0); + private: + struct ThreadTask { + ThreadTask(); + + std::vector tasks; + std::mutex mutex; + }; + + void executeTasks(); + + const auto &getThreadTask() const { + return m_threads[ThreadPool::getThreadId()]; + } + + std::vector> m_threads; + std::vector m_tasks; + void saveMap(); void saveKV(); diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 02f58bec04b..bc3c5a5dbae 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -398,22 +398,6 @@ void IOLoginDataLoad::loadPlayerGuild(const std::shared_ptr &player, DBR } } -void IOLoginDataLoad::loadPlayerStashItems(const std::shared_ptr &player, DBResult_ptr result) { - if (!result || !player) { - g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__); - return; - } - - Database &db = Database::getInstance(); - std::ostringstream query; - query << "SELECT `item_count`, `item_id` FROM `player_stash` WHERE `player_id` = " << player->getGUID(); - if ((result = db.storeQuery(query.str()))) { - do { - player->addItemOnStash(result->getNumber("item_id"), result->getNumber("item_count")); - } while (result->next()); - } -} - void IOLoginDataLoad::loadPlayerBestiaryCharms(const std::shared_ptr &player, DBResult_ptr result) { if (!result || !player) { g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__); @@ -690,22 +674,6 @@ void IOLoginDataLoad::loadPlayerInboxItems(const std::shared_ptr &player } } -void IOLoginDataLoad::loadPlayerStorageMap(const std::shared_ptr &player, DBResult_ptr result) { - if (!result || !player) { - g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__); - return; - } - - Database &db = Database::getInstance(); - std::ostringstream query; - query << "SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = " << player->getGUID(); - if ((result = db.storeQuery(query.str()))) { - do { - player->addStorageValue(result->getNumber("key"), result->getNumber("value"), true); - } while (result->next()); - } -} - void IOLoginDataLoad::loadPlayerVip(const std::shared_ptr &player, DBResult_ptr result) { if (!result || !player) { g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__); @@ -841,27 +809,6 @@ void IOLoginDataLoad::loadPlayerTaskHuntingClass(const std::shared_ptr & } } -void IOLoginDataLoad::loadPlayerForgeHistory(const std::shared_ptr &player, DBResult_ptr result) { - if (!result || !player) { - g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__); - return; - } - - std::ostringstream query; - query << "SELECT * FROM `forge_history` WHERE `player_id` = " << player->getGUID(); - if ((result = Database::getInstance().storeQuery(query.str()))) { - do { - auto actionEnum = magic_enum::enum_value(result->getNumber("action_type")); - ForgeHistory history; - history.actionType = actionEnum; - history.description = result->getString("description"); - history.createdAt = result->getNumber("done_at"); - history.success = result->getNumber("is_success"); - player->setForgeHistory(history); - } while (result->next()); - } -} - void IOLoginDataLoad::loadPlayerBosstiary(const std::shared_ptr &player, DBResult_ptr result) { if (!result) { g_logger().warn("[{}] - Result nullptr", __FUNCTION__); diff --git a/src/io/functions/iologindata_load_player.hpp b/src/io/functions/iologindata_load_player.hpp index 2d1a4305cf3..da3c3ad1095 100644 --- a/src/io/functions/iologindata_load_player.hpp +++ b/src/io/functions/iologindata_load_player.hpp @@ -23,7 +23,6 @@ class IOLoginDataLoad : public IOLoginData { static void loadPlayerSkill(const std::shared_ptr &player, const DBResult_ptr &result); static void loadPlayerKills(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerGuild(const std::shared_ptr &player, DBResult_ptr result); - static void loadPlayerStashItems(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerBestiaryCharms(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerInstantSpellList(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerInventoryItems(const std::shared_ptr &player, DBResult_ptr result); @@ -31,11 +30,9 @@ class IOLoginDataLoad : public IOLoginData { static void loadPlayerDepotItems(const std::shared_ptr &player, DBResult_ptr result); static void loadRewardItems(const std::shared_ptr &player); static void loadPlayerInboxItems(const std::shared_ptr &player, DBResult_ptr result); - static void loadPlayerStorageMap(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerVip(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerPreyClass(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerTaskHuntingClass(const std::shared_ptr &player, DBResult_ptr result); - static void loadPlayerForgeHistory(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerBosstiary(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerInitializeSystem(const std::shared_ptr &player); static void loadPlayerUpdateSystem(const std::shared_ptr &player); diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index c6b92514969..0d1a89d0781 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -17,6 +17,7 @@ #include "items/containers/depot/depotchest.hpp" #include "items/containers/inbox/inbox.hpp" #include "items/containers/rewards/reward.hpp" +#include "game/scheduling/save_manager.hpp" bool IOLoginDataSave::saveItems(const std::shared_ptr &player, const ItemBlockList &itemList, DBInsert &query_insert, PropWriteStream &propWriteStream) { if (!player) { @@ -144,17 +145,12 @@ bool IOLoginDataSave::saveItems(const std::shared_ptr &player, const Ite queue.pop_front(); } - // Execute query - if (!query_insert.execute()) { - g_logger().error("Error executing query."); - return false; - } return true; } bool IOLoginDataSave::savePlayerFirst(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerFirst] - Player nullptr: {}", __FUNCTION__); return false; } @@ -164,75 +160,84 @@ bool IOLoginDataSave::savePlayerFirst(const std::shared_ptr &player) { Database &db = Database::getInstance(); - std::ostringstream query; - query << "SELECT `save` FROM `players` WHERE `id` = " << player->getGUID(); - DBResult_ptr result = db.storeQuery(query.str()); + // Check if `save` flag is set + auto result = g_database().storeQuery(fmt::format("SELECT `save` FROM `players` WHERE `id` = {}", player->getGUID())); if (!result) { - g_logger().warn("[IOLoginData::savePlayer] - Error for select result query from player: {}", player->getName()); + g_logger().warn("[IOLoginDataSave::savePlayerFirst] - Error retrieving save flag for player: {}", player->getName()); return false; } + // Quick update if `save` flag is 0 if (result->getNumber("save") == 0) { - query.str(""); - query << "UPDATE `players` SET `lastlogin` = " << player->lastLoginSaved << ", `lastip` = " << player->lastIP << " WHERE `id` = " << player->getGUID(); - return db.executeQuery(query.str()); - } - - // First, an UPDATE query to write the player itself - query.str(""); - query << "UPDATE `players` SET "; - query << "`name` = " << db.escapeString(player->name) << ","; - query << "`level` = " << player->level << ","; - query << "`group_id` = " << player->group->id << ","; - query << "`vocation` = " << player->getVocationId() << ","; - query << "`health` = " << player->health << ","; - query << "`healthmax` = " << player->healthMax << ","; - query << "`experience` = " << player->experience << ","; - query << "`lookbody` = " << static_cast(player->defaultOutfit.lookBody) << ","; - query << "`lookfeet` = " << static_cast(player->defaultOutfit.lookFeet) << ","; - query << "`lookhead` = " << static_cast(player->defaultOutfit.lookHead) << ","; - query << "`looklegs` = " << static_cast(player->defaultOutfit.lookLegs) << ","; - query << "`looktype` = " << player->defaultOutfit.lookType << ","; - query << "`lookaddons` = " << static_cast(player->defaultOutfit.lookAddons) << ","; - query << "`lookmountbody` = " << static_cast(player->defaultOutfit.lookMountBody) << ","; - query << "`lookmountfeet` = " << static_cast(player->defaultOutfit.lookMountFeet) << ","; - query << "`lookmounthead` = " << static_cast(player->defaultOutfit.lookMountHead) << ","; - query << "`lookmountlegs` = " << static_cast(player->defaultOutfit.lookMountLegs) << ","; - query << "`lookfamiliarstype` = " << player->defaultOutfit.lookFamiliarsType << ","; - query << "`isreward` = " << static_cast(player->isDailyReward) << ","; - query << "`maglevel` = " << player->magLevel << ","; - query << "`mana` = " << player->mana << ","; - query << "`manamax` = " << player->manaMax << ","; - query << "`manaspent` = " << player->manaSpent << ","; - query << "`soul` = " << static_cast(player->soul) << ","; - if (player->town) { - query << "`town_id` = " << player->town->getID() << ","; - } - - const Position &loginPosition = player->getLoginPosition(); - query << "`posx` = " << loginPosition.getX() << ","; - query << "`posy` = " << loginPosition.getY() << ","; - query << "`posz` = " << loginPosition.getZ() << ","; - - query << "`prey_wildcard` = " << player->getPreyCards() << ","; - query << "`task_points` = " << player->getTaskHuntingPoints() << ","; - query << "`boss_points` = " << player->getBossPoints() << ","; - query << "`forge_dusts` = " << player->getForgeDusts() << ","; - query << "`forge_dust_level` = " << player->getForgeDustLevel() << ","; - query << "`randomize_mount` = " << static_cast(player->isRandomMounted()) << ","; - - query << "`cap` = " << (player->capacity / 100) << ","; - query << "`sex` = " << static_cast(player->sex) << ","; + auto quickUpdateTask = [queryStr = fmt::format( + "UPDATE `players` SET `lastlogin` = {}, `lastip` = {} WHERE `id` = {}", + player->lastLoginSaved, player->lastIP, player->getGUID() + )]() { + Database &db = Database::getInstance(); + if (!g_database().executeQuery(queryStr)) { + g_logger().warn("[SaveManager::quickUpdateTask] - Failed to execute quick update for player."); + return; + } + }; - if (player->lastLoginSaved != 0) { - query << "`lastlogin` = " << player->lastLoginSaved << ","; + g_saveManager().addTask(quickUpdateTask, "IOLoginDataSave::savePlayerFirst - quickUpdateTask"); + return true; } - if (player->lastIP != 0) { - query << "`lastip` = " << player->lastIP << ","; + // Build the list of column-value pairs + std::vector columns; + columns.reserve(result->countColumns()); + + // Basic Player Information + columns.push_back(fmt::format("`name` = {}", g_database().escapeString(player->name))); + columns.push_back(fmt::format("`level` = {}", player->level)); + columns.push_back(fmt::format("`group_id` = {}", player->group->id)); + columns.push_back(fmt::format("`vocation` = {}", player->getVocationId())); + columns.push_back(fmt::format("`health` = {}", player->health)); + columns.push_back(fmt::format("`healthmax` = {}", player->healthMax)); + columns.push_back(fmt::format("`experience` = {}", player->experience)); + + // Appearance Attributes + columns.push_back(fmt::format("`lookbody` = {}", static_cast(player->defaultOutfit.lookBody))); + columns.push_back(fmt::format("`lookfeet` = {}", static_cast(player->defaultOutfit.lookFeet))); + columns.push_back(fmt::format("`lookhead` = {}", static_cast(player->defaultOutfit.lookHead))); + columns.push_back(fmt::format("`looklegs` = {}", static_cast(player->defaultOutfit.lookLegs))); + columns.push_back(fmt::format("`looktype` = {}", player->defaultOutfit.lookType)); + columns.push_back(fmt::format("`lookaddons` = {}", static_cast(player->defaultOutfit.lookAddons))); + columns.push_back(fmt::format("`lookmountbody` = {}", static_cast(player->defaultOutfit.lookMountBody))); + columns.push_back(fmt::format("`lookmountfeet` = {}", static_cast(player->defaultOutfit.lookMountFeet))); + columns.push_back(fmt::format("`lookmounthead` = {}", static_cast(player->defaultOutfit.lookMountHead))); + columns.push_back(fmt::format("`lookmountlegs` = {}", static_cast(player->defaultOutfit.lookMountLegs))); + columns.push_back(fmt::format("`lookfamiliarstype` = {}", player->defaultOutfit.lookFamiliarsType)); + + columns.push_back(fmt::format("`isreward` = {}", static_cast(player->isDailyReward))); + columns.push_back(fmt::format("`maglevel` = {}", player->magLevel)); + columns.push_back(fmt::format("`mana` = {}", player->mana)); + + // Gameplay Stats + columns.push_back(fmt::format("`manamax` = {}", player->manaMax)); + columns.push_back(fmt::format("`manaspent` = {}", player->manaSpent)); + columns.push_back(fmt::format("`soul` = {}", static_cast(player->soul))); + columns.push_back(fmt::format("`town_id` = {}", player->town->getID())); + columns.push_back(fmt::format("`posx` = {}", player->getLoginPosition().getX())); + columns.push_back(fmt::format("`posy` = {}", player->getLoginPosition().getY())); + columns.push_back(fmt::format("`posz` = {}", player->getLoginPosition().getZ())); + columns.push_back(fmt::format("`prey_wildcard` = {}", player->getPreyCards())); + columns.push_back(fmt::format("`task_points` = {}", player->getTaskHuntingPoints())); + columns.push_back(fmt::format("`boss_points` = {}", player->getBossPoints())); + columns.push_back(fmt::format("`forge_dusts` = {}", player->getForgeDusts())); + columns.push_back(fmt::format("`forge_dust_level` = {}", player->getForgeDustLevel())); + columns.push_back(fmt::format("`randomize_mount` = {}", static_cast(player->isRandomMounted()))); + columns.push_back(fmt::format("`cap` = {}", (player->capacity / 100))); + columns.push_back(fmt::format("`sex` = {}", static_cast(player->sex))); + if (player->lastLoginSaved != 0) { + columns.push_back(fmt::format("`lastlogin` = {}", player->lastLoginSaved)); + } + if (player->lastIP) { + columns.push_back(fmt::format("`lastip` = {}", player->lastIP)); } - // serialize conditions + // Serialize conditions PropWriteStream propWriteStream; for (const auto &condition : player->conditions) { if (condition->isPersistent()) { @@ -243,234 +248,211 @@ bool IOLoginDataSave::savePlayerFirst(const std::shared_ptr &player) { size_t attributesSize; const char* attributes = propWriteStream.getStream(attributesSize); + if (attributesSize) { + columns.push_back(fmt::format("`conditions` = {}", g_database().escapeBlob(attributes, static_cast(attributesSize)))); + } - query << "`conditions` = " << db.escapeBlob(attributes, static_cast(attributesSize)) << ","; - + // Skull attributes, based on world type if (g_game().getWorldType() != WORLD_TYPE_PVP_ENFORCED) { int64_t skullTime = 0; - if (player->skullTicks > 0) { - auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - skullTime = now + player->skullTicks; + auto now = std::chrono::system_clock::now(); + skullTime = std::chrono::duration_cast(now.time_since_epoch()).count() + player->skullTicks; } - - query << "`skulltime` = " << skullTime << ","; - - Skulls_t skull = SKULL_NONE; - if (player->skull == SKULL_RED) { - skull = SKULL_RED; - } else if (player->skull == SKULL_BLACK) { - skull = SKULL_BLACK; - } - query << "`skull` = " << static_cast(skull) << ","; - } - - query << "`lastlogout` = " << player->getLastLogout() << ","; - query << "`balance` = " << player->bankBalance << ","; - query << "`offlinetraining_time` = " << player->getOfflineTrainingTime() / 1000 << ","; - query << "`offlinetraining_skill` = " << std::to_string(player->getOfflineTrainingSkill()) << ","; - query << "`stamina` = " << player->getStaminaMinutes() << ","; - query << "`skill_fist` = " << player->skills[SKILL_FIST].level << ","; - query << "`skill_fist_tries` = " << player->skills[SKILL_FIST].tries << ","; - query << "`skill_club` = " << player->skills[SKILL_CLUB].level << ","; - query << "`skill_club_tries` = " << player->skills[SKILL_CLUB].tries << ","; - query << "`skill_sword` = " << player->skills[SKILL_SWORD].level << ","; - query << "`skill_sword_tries` = " << player->skills[SKILL_SWORD].tries << ","; - query << "`skill_axe` = " << player->skills[SKILL_AXE].level << ","; - query << "`skill_axe_tries` = " << player->skills[SKILL_AXE].tries << ","; - query << "`skill_dist` = " << player->skills[SKILL_DISTANCE].level << ","; - query << "`skill_dist_tries` = " << player->skills[SKILL_DISTANCE].tries << ","; - query << "`skill_shielding` = " << player->skills[SKILL_SHIELD].level << ","; - query << "`skill_shielding_tries` = " << player->skills[SKILL_SHIELD].tries << ","; - query << "`skill_fishing` = " << player->skills[SKILL_FISHING].level << ","; - query << "`skill_fishing_tries` = " << player->skills[SKILL_FISHING].tries << ","; - query << "`skill_critical_hit_chance` = " << player->skills[SKILL_CRITICAL_HIT_CHANCE].level << ","; - query << "`skill_critical_hit_chance_tries` = " << player->skills[SKILL_CRITICAL_HIT_CHANCE].tries << ","; - query << "`skill_critical_hit_damage` = " << player->skills[SKILL_CRITICAL_HIT_DAMAGE].level << ","; - query << "`skill_critical_hit_damage_tries` = " << player->skills[SKILL_CRITICAL_HIT_DAMAGE].tries << ","; - query << "`skill_life_leech_chance` = " << player->skills[SKILL_LIFE_LEECH_CHANCE].level << ","; - query << "`skill_life_leech_chance_tries` = " << player->skills[SKILL_LIFE_LEECH_CHANCE].tries << ","; - query << "`skill_life_leech_amount` = " << player->skills[SKILL_LIFE_LEECH_AMOUNT].level << ","; - query << "`skill_life_leech_amount_tries` = " << player->skills[SKILL_LIFE_LEECH_AMOUNT].tries << ","; - query << "`skill_mana_leech_chance` = " << player->skills[SKILL_MANA_LEECH_CHANCE].level << ","; - query << "`skill_mana_leech_chance_tries` = " << player->skills[SKILL_MANA_LEECH_CHANCE].tries << ","; - query << "`skill_mana_leech_amount` = " << player->skills[SKILL_MANA_LEECH_AMOUNT].level << ","; - query << "`skill_mana_leech_amount_tries` = " << player->skills[SKILL_MANA_LEECH_AMOUNT].tries << ","; - query << "`manashield` = " << player->getManaShield() << ","; - query << "`max_manashield` = " << player->getMaxManaShield() << ","; - query << "`xpboost_value` = " << player->getXpBoostPercent() << ","; - query << "`xpboost_stamina` = " << player->getXpBoostTime() << ","; - query << "`quickloot_fallback` = " << (player->quickLootFallbackToMainContainer ? 1 : 0) << ","; - - if (!player->isOffline()) { - auto now = std::chrono::system_clock::now(); - auto lastLoginSaved = std::chrono::system_clock::from_time_t(player->lastLoginSaved); - query << "`onlinetime` = `onlinetime` + " << std::chrono::duration_cast(now - lastLoginSaved).count() << ","; - } - - for (int i = 1; i <= 8; i++) { - query << "`blessings" << i << "`" - << " = " << static_cast(player->getBlessingCount(static_cast(i))) << ((i == 8) ? " " : ","); - } - query << " WHERE `id` = " << player->getGUID(); - - if (!db.executeQuery(query.str())) { - return false; - } - return true; -} - -bool IOLoginDataSave::savePlayerStash(const std::shared_ptr &player) { - if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } - - Database &db = Database::getInstance(); - std::ostringstream query; - query << "DELETE FROM `player_stash` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { - return false; - } - - query.str(""); - - DBInsert stashQuery("INSERT INTO `player_stash` (`player_id`,`item_id`,`item_count`) VALUES "); - for (const auto &[itemId, itemCount] : player->getStashItems()) { - query << player->getGUID() << ',' << itemId << ',' << itemCount; - if (!stashQuery.addRow(query)) { - return false; + columns.push_back(fmt::format("`skulltime` = {}", skullTime)); + columns.push_back(fmt::format("`skull` = {}", static_cast(player->skull))); + } + + // Additional fields + columns.push_back(fmt::format("`lastlogout` = {}", player->getLastLogout())); + columns.push_back(fmt::format("`balance` = {}", player->bankBalance)); + columns.push_back(fmt::format("`offlinetraining_time` = {}", player->getOfflineTrainingTime() / 1000)); + columns.push_back(fmt::format("`offlinetraining_skill` = {}", player->getOfflineTrainingSkill())); + columns.push_back(fmt::format("`skill_fist` = {}", player->skills[SKILL_FIST].level)); + columns.push_back(fmt::format("`skill_fist_tries` = {}", player->skills[SKILL_FIST].tries)); + columns.push_back(fmt::format("`skill_club` = {}", player->skills[SKILL_CLUB].level)); + columns.push_back(fmt::format("`skill_club_tries` = {}", player->skills[SKILL_CLUB].tries)); + columns.push_back(fmt::format("`skill_sword` = {}", player->skills[SKILL_SWORD].level)); + columns.push_back(fmt::format("`skill_sword_tries` = {}", player->skills[SKILL_SWORD].tries)); + columns.push_back(fmt::format("`skill_axe` = {}", player->skills[SKILL_AXE].level)); + columns.push_back(fmt::format("`skill_axe_tries` = {}", player->skills[SKILL_AXE].tries)); + columns.push_back(fmt::format("`skill_dist` = {}", player->skills[SKILL_DISTANCE].level)); + columns.push_back(fmt::format("`skill_dist_tries` = {}", player->skills[SKILL_DISTANCE].tries)); + columns.push_back(fmt::format("`skill_shielding` = {}", player->skills[SKILL_SHIELD].level)); + columns.push_back(fmt::format("`skill_shielding_tries` = {}", player->skills[SKILL_SHIELD].tries)); + columns.push_back(fmt::format("`skill_fishing` = {}", player->skills[SKILL_FISHING].level)); + columns.push_back(fmt::format("`skill_fishing_tries` = {}", player->skills[SKILL_FISHING].tries)); + columns.push_back(fmt::format("`skill_critical_hit_chance` = {}", player->skills[SKILL_CRITICAL_HIT_CHANCE].level)); + columns.push_back(fmt::format("`skill_critical_hit_chance_tries` = {}", player->skills[SKILL_CRITICAL_HIT_CHANCE].tries)); + columns.push_back(fmt::format("`skill_critical_hit_damage` = {}", player->skills[SKILL_CRITICAL_HIT_DAMAGE].level)); + columns.push_back(fmt::format("`skill_critical_hit_damage_tries` = {}", player->skills[SKILL_CRITICAL_HIT_DAMAGE].tries)); + columns.push_back(fmt::format("`skill_life_leech_chance` = {}", player->skills[SKILL_LIFE_LEECH_CHANCE].level)); + columns.push_back(fmt::format("`skill_life_leech_chance_tries` = {}", player->skills[SKILL_LIFE_LEECH_CHANCE].tries)); + columns.push_back(fmt::format("`skill_life_leech_amount` = {}", player->skills[SKILL_LIFE_LEECH_AMOUNT].level)); + columns.push_back(fmt::format("`skill_life_leech_amount_tries` = {}", player->skills[SKILL_LIFE_LEECH_AMOUNT].tries)); + columns.push_back(fmt::format("`skill_mana_leech_chance` = {}", player->skills[SKILL_MANA_LEECH_CHANCE].level)); + columns.push_back(fmt::format("`skill_mana_leech_chance_tries` = {}", player->skills[SKILL_MANA_LEECH_CHANCE].tries)); + columns.push_back(fmt::format("`skill_mana_leech_amount` = {}", player->skills[SKILL_MANA_LEECH_AMOUNT].level)); + columns.push_back(fmt::format("`skill_mana_leech_amount_tries` = {}", player->skills[SKILL_MANA_LEECH_AMOUNT].tries)); + columns.push_back(fmt::format("`stamina` = {}", player->getStaminaMinutes())); + columns.push_back(fmt::format("`manashield` = {}", player->getManaShield())); + columns.push_back(fmt::format("`max_manashield` = {}", player->getMaxManaShield())); + columns.push_back(fmt::format("`xpboost_value` = {}", player->getXpBoostPercent())); + columns.push_back(fmt::format("`xpboost_stamina` = {}", player->getXpBoostTime())); + columns.push_back(fmt::format("`quickloot_fallback` = {}", player->quickLootFallbackToMainContainer ? 1 : 0)); + + // Blessings + for (int i = 1; i <= 8; ++i) { + columns.push_back(fmt::format("`blessings{}` = {}", i, static_cast(player->getBlessingCount(static_cast(i))))); + } + + // Create the task for the query execution + auto savePlayerTask = [joinColums = std::move(columns), playerName = player->getName(), playerGUID = player->getGUID()]() { + // Now join the columns into a single string + std::string setClause = fmt::to_string(fmt::join(joinColums, ", ")); + // Construct the final query + std::string queryStr = fmt::format("UPDATE `players` SET {} WHERE `id` = {}", setClause, playerGUID); + if (!g_database().executeQuery(queryStr)) { + g_logger().warn("[SaveManager::savePlayerTask] - Error executing player first save for player: {}", playerName); } - } + }; - if (!stashQuery.execute()) { - return false; - } + // Add the task to the SaveManager + g_saveManager().addTask(savePlayerTask, "IOLoginData::savePlayerFirst - savePlayerTask"); return true; } bool IOLoginDataSave::savePlayerSpells(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } - - Database &db = Database::getInstance(); - std::ostringstream query; - query << "DELETE FROM `player_spells` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { + g_logger().warn("[IOLoginDataSave::savePlayerSpells] - Player nullptr: {}", __FUNCTION__); return false; } - query.str(""); - - DBInsert spellsQuery("INSERT INTO `player_spells` (`player_id`, `name` ) VALUES "); + auto deleteQueryStr = fmt::format("DELETE FROM `player_spells` WHERE `player_id` = {}", player->getGUID()); + DBInsert spellsQuery("INSERT INTO `player_spells` (`player_id`, `name`) VALUES "); for (const std::string &spellName : player->learnedInstantSpellList) { - query << player->getGUID() << ',' << db.escapeString(spellName); - if (!spellsQuery.addRow(query)) { + std::string row = fmt::format("{},{}", player->getGUID(), g_database().escapeString(spellName)); + if (!spellsQuery.addRow(row)) { + g_logger().warn("[IOLoginDataSave::savePlayerSpells] - Failed to add row for player spells"); return false; } } - if (!spellsQuery.execute()) { - return false; - } + auto spellsSaveTask = [deleteQueryStr, spellsQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().error("[SaveManager::spellsSaveTask] - Failed to execute delete query for player spells"); + return; + } + + if (!spellsQuery.execute()) { + g_logger().warn("[SaveManager::spellsSaveTask] - Failed to execute insert query for player spells"); + } + }; + + g_saveManager().addTask(spellsSaveTask, "IOLoginData::savePlayerSpells - spellsSaveTask"); return true; } bool IOLoginDataSave::savePlayerKills(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } - - Database &db = Database::getInstance(); - std::ostringstream query; - query << "DELETE FROM `player_kills` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { + g_logger().warn("[IOLoginDataSave::savePlayerKills] - Player nullptr: {}", __FUNCTION__); return false; } - query.str(""); - + auto deleteQueryStr = fmt::format("DELETE FROM `player_kills` WHERE `player_id` = {}", player->getGUID()); DBInsert killsQuery("INSERT INTO `player_kills` (`player_id`, `target`, `time`, `unavenged`) VALUES"); for (const auto &kill : player->unjustifiedKills) { - query << player->getGUID() << ',' << kill.target << ',' << kill.time << ',' << kill.unavenged; - if (!killsQuery.addRow(query)) { + std::string row = fmt::format("{},{},{},{}", player->getGUID(), kill.target, kill.time, kill.unavenged); + if (!killsQuery.addRow(row)) { + g_logger().warn("[IOLoginDataSave::savePlayerKills] - Failed to add row for player kills"); return false; } } - if (!killsQuery.execute()) { - return false; - } + auto killsSaveTask = [deleteQueryStr, killsQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().warn("[SaveManager::killsSaveTask] - Failed to execute delete query for player kills"); + return; + } + + if (!killsQuery.execute()) { + g_logger().warn("[SaveManager::killsSaveTask] - Failed to execute insert query for player kills"); + } + }; + + g_saveManager().addTask(killsSaveTask, "IOLoginData::savePlayerKills - killsSaveTask"); return true; } bool IOLoginDataSave::savePlayerBestiarySystem(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerBestiarySystem] - Player nullptr: {}", __FUNCTION__); return false; } Database &db = Database::getInstance(); - std::ostringstream query; - query << "UPDATE `player_charms` SET "; - query << "`charm_points` = " << player->charmPoints << ","; - query << "`charm_expansion` = " << ((player->charmExpansion) ? 1 : 0) << ","; - query << "`rune_wound` = " << player->charmRuneWound << ","; - query << "`rune_enflame` = " << player->charmRuneEnflame << ","; - query << "`rune_poison` = " << player->charmRunePoison << ","; - query << "`rune_freeze` = " << player->charmRuneFreeze << ","; - query << "`rune_zap` = " << player->charmRuneZap << ","; - query << "`rune_curse` = " << player->charmRuneCurse << ","; - query << "`rune_cripple` = " << player->charmRuneCripple << ","; - query << "`rune_parry` = " << player->charmRuneParry << ","; - query << "`rune_dodge` = " << player->charmRuneDodge << ","; - query << "`rune_adrenaline` = " << player->charmRuneAdrenaline << ","; - query << "`rune_numb` = " << player->charmRuneNumb << ","; - query << "`rune_cleanse` = " << player->charmRuneCleanse << ","; - query << "`rune_bless` = " << player->charmRuneBless << ","; - query << "`rune_scavenge` = " << player->charmRuneScavenge << ","; - query << "`rune_gut` = " << player->charmRuneGut << ","; - query << "`rune_low_blow` = " << player->charmRuneLowBlow << ","; - query << "`rune_divine` = " << player->charmRuneDivine << ","; - query << "`rune_vamp` = " << player->charmRuneVamp << ","; - query << "`rune_void` = " << player->charmRuneVoid << ","; - query << "`UsedRunesBit` = " << player->UsedRunesBit << ","; - query << "`UnlockedRunesBit` = " << player->UnlockedRunesBit << ","; - PropWriteStream propBestiaryStream; for (const auto &trackedType : player->getCyclopediaMonsterTrackerSet(false)) { propBestiaryStream.write(trackedType->info.raceid); } size_t trackerSize; const char* trackerList = propBestiaryStream.getStream(trackerSize); - query << " `tracker list` = " << db.escapeBlob(trackerList, static_cast(trackerSize)); - query << " WHERE `player_guid` = " << player->getGUID(); + auto escapedTrackerList = g_database().escapeBlob(trackerList, static_cast(trackerSize)); + + std::string updateQuery = fmt::format( + "UPDATE `player_charms` SET `charm_points` = {}, `charm_expansion` = {}, " + "`rune_wound` = {}, `rune_enflame` = {}, `rune_poison` = {}, `rune_freeze` = {}, " + "`rune_zap` = {}, `rune_curse` = {}, `rune_cripple` = {}, `rune_parry` = {}, " + "`rune_dodge` = {}, `rune_adrenaline` = {}, `rune_numb` = {}, `rune_cleanse` = {}, " + "`rune_bless` = {}, `rune_scavenge` = {}, `rune_gut` = {}, `rune_low_blow` = {}, " + "`rune_divine` = {}, `rune_vamp` = {}, `rune_void` = {}, `UsedRunesBit` = {}, " + "`UnlockedRunesBit` = {}, `tracker list` = {} WHERE `player_guid` = {}", + player->charmPoints, + player->charmExpansion ? 1 : 0, + player->charmRuneWound, + player->charmRuneEnflame, + player->charmRunePoison, + player->charmRuneFreeze, + player->charmRuneZap, + player->charmRuneCurse, + player->charmRuneCripple, + player->charmRuneParry, + player->charmRuneDodge, + player->charmRuneAdrenaline, + player->charmRuneNumb, + player->charmRuneCleanse, + player->charmRuneBless, + player->charmRuneScavenge, + player->charmRuneGut, + player->charmRuneLowBlow, + player->charmRuneDivine, + player->charmRuneVamp, + player->charmRuneVoid, + player->UsedRunesBit, + player->UnlockedRunesBit, + escapedTrackerList, + player->getGUID() + ); + + auto bestiarySaveTask = [updateQuery]() { + if (!g_database().executeQuery(updateQuery)) { + g_logger().warn("[SaveManager::bestiarySaveTask] - Failed to execute bestiary data update query"); + } + }; - if (!db.executeQuery(query.str())) { - g_logger().warn("[IOLoginData::savePlayer] - Error saving bestiary data from player: {}", player->getName()); - return false; - } + g_saveManager().addTask(bestiarySaveTask, "IOLoginData::savePlayerBestiarySystem - bestiarySaveTask"); return true; } bool IOLoginDataSave::savePlayerItem(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerItem] - Player nullptr: {}", __FUNCTION__); return false; } - Database &db = Database::getInstance(); PropWriteStream propWriteStream; - std::ostringstream query; - query << "DELETE FROM `player_items` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { - g_logger().warn("[IOLoginData::savePlayer] - Error delete query 'player_items' from player: {}", player->getName()); - return false; - } + std::string deleteQueryStr = fmt::format("DELETE FROM `player_items` WHERE `player_id` = {}", player->getGUID()); DBInsert itemsQuery("INSERT INTO `player_items` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); @@ -483,320 +465,273 @@ bool IOLoginDataSave::savePlayerItem(const std::shared_ptr &player) { } if (!saveItems(player, itemList, itemsQuery, propWriteStream)) { - g_logger().warn("[IOLoginData::savePlayer] - Failed for save items from player: {}", player->getName()); + g_logger().warn("[IOLoginDataSave::savePlayerItem] - Failed to save items for player: {}", player->getName()); return false; } + + auto itemsSaveTask = [deleteQueryStr, itemsQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().warn("[SaveManager::itemsSaveTask] - Failed to execute delete query for player items"); + return; + } + + if (!itemsQuery.execute()) { + g_logger().warn("[SaveManager::itemsSaveTask] - Failed to execute insert query for player items"); + } + }; + + g_saveManager().addTask(itemsSaveTask, "IOLoginData::savePlayerItem - itemsSaveTask"); return true; } bool IOLoginDataSave::savePlayerDepotItems(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerDepotItems] - Player nullptr: {}", __FUNCTION__); return false; } - Database &db = Database::getInstance(); - PropWriteStream propWriteStream; - ItemDepotList depotList; - if (player->lastDepotId != -1) { - std::ostringstream query; - query << "DELETE FROM `player_depotitems` WHERE `player_id` = " << player->getGUID(); + auto deleteQueryStr = fmt::format("DELETE FROM `player_depotitems` WHERE `player_id` = {}", player->getGUID()); + DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); - if (!db.executeQuery(query.str())) { - return false; + ItemDepotList depotList; + for (const auto &[pid, depotChest] : player->depotChests) { + for (const std::shared_ptr &item : depotChest->getItemList()) { + depotList.emplace_back(pid, item); } + } - query.str(""); - - DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + PropWriteStream propWriteStream; + if (!saveItems(player, depotList, depotQuery, propWriteStream)) { + g_logger().warn("[IOLoginDataSave::savePlayerDepotItems] - Failed to save depot items for player: {}", player->getName()); + return false; + } - for (const auto &[pid, depotChest] : player->depotChests) { - for (const std::shared_ptr &item : depotChest->getItemList()) { - depotList.emplace_back(pid, item); - } + auto depotItemsSaveTask = [deleteQueryStr, depotQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().warn("[SaveManager::depotItemsSaveTask] - Failed to execute delete query for depot items"); + return; } - if (!saveItems(player, depotList, depotQuery, propWriteStream)) { - return false; + if (!depotQuery.execute()) { + g_logger().warn("[SaveManager::depotItemsSaveTask] - Failed to execute insert query for depot items"); } - return true; - } + }; + + g_saveManager().addTask(depotItemsSaveTask, "IOLoginData::savePlayerDepotItems - depotItemsSaveTask"); return true; } bool IOLoginDataSave::saveRewardItems(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::saveRewardItems] - Player nullptr: {}", __FUNCTION__); return false; } - std::ostringstream query; - query << "DELETE FROM `player_rewards` WHERE `player_id` = " << player->getGUID(); - - if (!Database::getInstance().executeQuery(query.str())) { - return false; - } + auto deleteQueryStr = fmt::format("DELETE FROM `player_rewards` WHERE `player_id` = {}", player->getGUID()); + DBInsert rewardQuery("INSERT INTO `player_rewards` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + ItemRewardList rewardListItems; std::vector rewardList; player->getRewardList(rewardList); - ItemRewardList rewardListItems; if (!rewardList.empty()) { for (const auto &rewardId : rewardList) { - auto reward = player->getReward(rewardId, false); - if (!reward->empty() && (getTimeMsNow() - rewardId <= 1000 * 60 * 60 * 24 * 7)) { + const auto &reward = player->getReward(rewardId, false); + if (reward && !reward->empty() && (getTimeMsNow() - rewardId <= 1000 * 60 * 60 * 24 * 7)) { rewardListItems.emplace_back(0, reward); } } + } - DBInsert rewardQuery("INSERT INTO `player_rewards` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); - PropWriteStream propWriteStream; - if (!saveItems(player, rewardListItems, rewardQuery, propWriteStream)) { - return false; - } + PropWriteStream propWriteStream; + if (!saveItems(player, rewardListItems, rewardQuery, propWriteStream)) { + g_logger().warn("[IOLoginDataSave::saveRewardItems] - Failed to save reward items for the player: {}", player->getName()); + return false; } + + auto rewardItemsSaveTask = [deleteQueryStr, rewardQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().warn("[SaveManager::rewardItemsSaveTask] - Failed to execute delete query for reward items"); + return; + } + + if (!rewardQuery.execute()) { + g_logger().warn("[SaveManager::rewardItemsSaveTask] - Failed to execute insert query for reward items"); + } + }; + + g_saveManager().addTask(rewardItemsSaveTask, "IOLoginDataSave::saveRewardItems - rewardItemsSaveTask"); return true; } bool IOLoginDataSave::savePlayerInbox(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } - - Database &db = Database::getInstance(); - PropWriteStream propWriteStream; - ItemInboxList inboxList; - std::ostringstream query; - query << "DELETE FROM `player_inboxitems` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { + g_logger().warn("[IOLoginDataSave::savePlayerInbox] - Player nullptr: {}", __FUNCTION__); return false; } - query.str(""); + auto deleteQueryStr = fmt::format("DELETE FROM `player_inboxitems` WHERE `player_id` = {}", player->getGUID()); DBInsert inboxQuery("INSERT INTO `player_inboxitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + ItemInboxList inboxList; for (const auto &item : player->getInbox()->getItemList()) { inboxList.emplace_back(0, item); } + PropWriteStream propWriteStream; if (!saveItems(player, inboxList, inboxQuery, propWriteStream)) { + g_logger().warn("[IOLoginDataSave::savePlayerInbox] - Failed to save inbox items for player: {}", player->getName()); return false; } + + auto inboxSaveTask = [deleteQueryStr, inboxQuery]() mutable { + if (!g_database().executeQuery(deleteQueryStr)) { + g_logger().warn("[SaveManager::inboxSaveTask] - Failed to execute delete query for inbox items"); + return; + } + + if (!inboxQuery.execute()) { + g_logger().warn("[SaveManager::inboxSaveTask] - Failed to execute insert query for inbox items"); + } + }; + + g_saveManager().addTask(inboxSaveTask, "IOLoginDataSave::savePlayerInbox - inboxSaveTask"); return true; } bool IOLoginDataSave::savePlayerPreyClass(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerPreyClass] - Player nullptr: {}", __FUNCTION__); return false; } - Database &db = Database::getInstance(); if (g_configManager().getBoolean(PREY_ENABLED)) { - std::ostringstream query; + DBInsert preyQuery("INSERT INTO player_prey " + "(`player_id`, `slot`, `state`, `raceid`, `option`, `bonus_type`, `bonus_rarity`, " + "`bonus_percentage`, `bonus_time`, `free_reroll`, `monster_list`) VALUES "); + preyQuery.upsert({ "state", "raceid", "option", "bonus_type", "bonus_rarity", + "bonus_percentage", "bonus_time", "free_reroll", "monster_list" }); + + auto playerGUID = player->getGUID(); for (uint8_t slotId = PreySlot_First; slotId <= PreySlot_Last; slotId++) { if (const auto &slot = player->getPreySlotById(static_cast(slotId))) { - query.str(std::string()); - query << "INSERT INTO player_prey (`player_id`, `slot`, `state`, `raceid`, `option`, `bonus_type`, `bonus_rarity`, `bonus_percentage`, `bonus_time`, `free_reroll`, `monster_list`) " - << "VALUES (" << player->getGUID() << ", " - << static_cast(slot->id) << ", " - << static_cast(slot->state) << ", " - << slot->selectedRaceId << ", " - << static_cast(slot->option) << ", " - << static_cast(slot->bonus) << ", " - << static_cast(slot->bonusRarity) << ", " - << slot->bonusPercentage << ", " - << slot->bonusTimeLeft << ", " - << slot->freeRerollTimeStamp << ", "; - PropWriteStream propPreyStream; - std::ranges::for_each(slot->raceIdList, [&propPreyStream](uint16_t raceId) { + for (uint16_t raceId : slot->raceIdList) { propPreyStream.write(raceId); - }); + } size_t preySize; const char* preyList = propPreyStream.getStream(preySize); - query << db.escapeBlob(preyList, static_cast(preySize)) << ")"; - - query << " ON DUPLICATE KEY UPDATE " - << "`state` = VALUES(`state`), " - << "`raceid` = VALUES(`raceid`), " - << "`option` = VALUES(`option`), " - << "`bonus_type` = VALUES(`bonus_type`), " - << "`bonus_rarity` = VALUES(`bonus_rarity`), " - << "`bonus_percentage` = VALUES(`bonus_percentage`), " - << "`bonus_time` = VALUES(`bonus_time`), " - << "`free_reroll` = VALUES(`free_reroll`), " - << "`monster_list` = VALUES(`monster_list`)"; - - if (!db.executeQuery(query.str())) { - g_logger().warn("[IOLoginData::savePlayer] - Error saving prey slot data from player: {}", player->getName()); + auto escapedPreyList = g_database().escapeBlob(preyList, static_cast(preySize)); + + // Format row data for batch insert + auto row = fmt::format("{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}", playerGUID, static_cast(slot->id), static_cast(slot->state), slot->selectedRaceId, static_cast(slot->option), static_cast(slot->bonus), static_cast(slot->bonusRarity), slot->bonusPercentage, slot->bonusTimeLeft, slot->freeRerollTimeStamp, escapedPreyList); + + if (!preyQuery.addRow(row)) { + g_logger().warn("[IOLoginDataSave::savePlayerPreyClass] - Failed to add prey slot data for player: {}", player->getName()); return false; } } } + + auto preySaveTask = [preyQuery]() mutable { + Database &db = Database::getInstance(); + if (!preyQuery.execute()) { + g_logger().warn("[SaveManager::preySaveTask] - Failed to execute prey slot data insertion"); + } + }; + + g_saveManager().addTask(preySaveTask, "IOLoginDataSave::savePlayerPreyClass - preySaveTask"); } + return true; } bool IOLoginDataSave::savePlayerTaskHuntingClass(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + g_logger().warn("[IOLoginDataSave::savePlayerTaskHuntingClass] - Player nullptr: {}", __FUNCTION__); return false; } - Database &db = Database::getInstance(); if (g_configManager().getBoolean(TASK_HUNTING_ENABLED)) { - std::ostringstream query; + DBInsert taskHuntQuery("INSERT INTO `player_taskhunt` " + "(`player_id`, `slot`, `state`, `raceid`, `upgrade`, `rarity`, " + "`kills`, `disabled_time`, `free_reroll`, `monster_list`) VALUES "); + taskHuntQuery.upsert({ "state", "raceid", "upgrade", "rarity", "kills", "disabled_time", + "free_reroll", "monster_list" }); + + auto playerGUID = player->getGUID(); for (uint8_t slotId = PreySlot_First; slotId <= PreySlot_Last; slotId++) { if (const auto &slot = player->getTaskHuntingSlotById(static_cast(slotId))) { - query.str(""); - query << "INSERT INTO `player_taskhunt` (`player_id`, `slot`, `state`, `raceid`, `upgrade`, `rarity`, `kills`, `disabled_time`, `free_reroll`, `monster_list`) VALUES ("; - query << player->getGUID() << ", "; - query << static_cast(slot->id) << ", "; - query << static_cast(slot->state) << ", "; - query << slot->selectedRaceId << ", "; - query << (slot->upgrade ? 1 : 0) << ", "; - query << static_cast(slot->rarity) << ", "; - query << slot->currentKills << ", "; - query << slot->disabledUntilTimeStamp << ", "; - query << slot->freeRerollTimeStamp << ", "; - PropWriteStream propTaskHuntingStream; - std::ranges::for_each(slot->raceIdList, [&propTaskHuntingStream](uint16_t raceId) { + for (uint16_t raceId : slot->raceIdList) { propTaskHuntingStream.write(raceId); - }); + } size_t taskHuntingSize; const char* taskHuntingList = propTaskHuntingStream.getStream(taskHuntingSize); - query << db.escapeBlob(taskHuntingList, static_cast(taskHuntingSize)) << ")"; - - query << " ON DUPLICATE KEY UPDATE " - << "`state` = VALUES(`state`), " - << "`raceid` = VALUES(`raceid`), " - << "`upgrade` = VALUES(`upgrade`), " - << "`rarity` = VALUES(`rarity`), " - << "`kills` = VALUES(`kills`), " - << "`disabled_time` = VALUES(`disabled_time`), " - << "`free_reroll` = VALUES(`free_reroll`), " - << "`monster_list` = VALUES(`monster_list`)"; - - if (!db.executeQuery(query.str())) { - g_logger().warn("[IOLoginData::savePlayer] - Error saving task hunting slot data from player: {}", player->getName()); + auto escapedTaskHuntingList = g_database().escapeBlob(taskHuntingList, static_cast(taskHuntingSize)); + + // Construct row for batch insert + auto row = fmt::format("{}, {}, {}, {}, {}, {}, {}, {}, {}, {}", playerGUID, static_cast(slot->id), static_cast(slot->state), slot->selectedRaceId, slot->upgrade ? 1 : 0, static_cast(slot->rarity), slot->currentKills, slot->disabledUntilTimeStamp, slot->freeRerollTimeStamp, escapedTaskHuntingList); + + if (!taskHuntQuery.addRow(row)) { + g_logger().warn("[IOLoginDataSave::savePlayerTaskHuntingClass] - Failed to add task hunting slot data for player: {}", player->getName()); return false; } } } - } - return true; -} -bool IOLoginDataSave::savePlayerForgeHistory(const std::shared_ptr &player) { - if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } + auto taskHuntingSaveTask = [taskHuntQuery]() mutable { + Database &db = Database::getInstance(); + if (!taskHuntQuery.execute()) { + g_logger().warn("[SaveManager::taskHuntingSaveTask] - Failed to execute task hunting data insertion"); + } + }; - std::ostringstream query; - query << "DELETE FROM `forge_history` WHERE `player_id` = " << player->getGUID(); - if (!Database::getInstance().executeQuery(query.str())) { - return false; + g_saveManager().addTask(taskHuntingSaveTask, "IOLoginDataSave::savePlayerTaskHuntingClass - taskHuntingSaveTask"); } - query.str(""); - DBInsert insertQuery("INSERT INTO `forge_history` (`player_id`, `action_type`, `description`, `done_at`, `is_success`) VALUES"); - for (const auto &history : player->getForgeHistory()) { - const auto stringDescription = Database::getInstance().escapeString(history.description); - auto actionString = magic_enum::enum_integer(history.actionType); - // Append query informations - query << player->getGUID() << ',' - << std::to_string(actionString) << ',' - << stringDescription << ',' - << history.createdAt << ',' - << history.success; - - if (!insertQuery.addRow(query)) { - return false; - } - } - if (!insertQuery.execute()) { - return false; - } return true; } bool IOLoginDataSave::savePlayerBosstiary(const std::shared_ptr &player) { if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); - return false; - } - - std::ostringstream query; - query << "DELETE FROM `player_bosstiary` WHERE `player_id` = " << player->getGUID(); - if (!Database::getInstance().executeQuery(query.str())) { + g_logger().warn("[IOLoginDataSave::savePlayerBosstiary] - Player nullptr: {}", __FUNCTION__); return false; } - query.str(""); - DBInsert insertQuery("INSERT INTO `player_bosstiary` (`player_id`, `bossIdSlotOne`, `bossIdSlotTwo`, `removeTimes`, `tracker`) VALUES"); + DBInsert insertQuery("INSERT INTO `player_bosstiary` " + "(`player_id`, `bossIdSlotOne`, `bossIdSlotTwo`, `removeTimes`, `tracker`) VALUES "); + insertQuery.upsert({ "bossIdSlotOne", "bossIdSlotTwo", "removeTimes", "tracker" }); - // Bosstiary tracker + // Prepare tracker data using PropWriteStream PropWriteStream stream; for (const auto &monsterType : player->getCyclopediaMonsterTrackerSet(true)) { if (!monsterType) { continue; } - stream.write(monsterType->info.raceid); } - size_t size; - const char* chars = stream.getStream(size); - // Append query informations - query << player->getGUID() << ',' - << player->getSlotBossId(1) << ',' - << player->getSlotBossId(2) << ',' - << std::to_string(player->getRemoveTimes()) << ',' - << Database::getInstance().escapeBlob(chars, static_cast(size)); - - if (!insertQuery.addRow(query)) { - return false; - } - if (!insertQuery.execute()) { - return false; - } + size_t size; + const char* trackerBlob = stream.getStream(size); + auto escapedTrackerBlob = g_database().escapeBlob(trackerBlob, static_cast(size)); - return true; -} + // Construct row for batch insert + auto row = fmt::format("{}, {}, {}, {}, {}", player->getGUID(), player->getSlotBossId(1), player->getSlotBossId(2), player->getRemoveTimes(), escapedTrackerBlob); -bool IOLoginDataSave::savePlayerStorage(const std::shared_ptr &player) { - if (!player) { - g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + if (!insertQuery.addRow(row)) { + g_logger().warn("[IOLoginDataSave::savePlayerBosstiary] - Failed to add bosstiary data for player: {}", player->getName()); return false; } - Database &db = Database::getInstance(); - std::ostringstream query; - query << "DELETE FROM `player_storage` WHERE `player_id` = " << player->getGUID(); - if (!db.executeQuery(query.str())) { - return false; - } - - query.str(""); - - DBInsert storageQuery("INSERT INTO `player_storage` (`player_id`, `key`, `value`) VALUES "); - player->genReservedStorageRange(); - - for (const auto &[key, value] : player->storageMap) { - query << player->getGUID() << ',' << key << ',' << value; - if (!storageQuery.addRow(query)) { - return false; + auto bosstiarySaveTask = [insertQuery]() mutable { + if (!insertQuery.execute()) { + g_logger().warn("[SaveManager::bosstiarySaveTask] - Error executing bosstiary data insertion"); } - } + }; - if (!storageQuery.execute()) { - return false; - } + g_saveManager().addTask(bosstiarySaveTask, "IOLoginDataSave::savePlayerBosstiary - bosstiarySaveTask"); return true; } diff --git a/src/io/functions/iologindata_save_player.hpp b/src/io/functions/iologindata_save_player.hpp index abafc2c186a..c888795d74f 100644 --- a/src/io/functions/iologindata_save_player.hpp +++ b/src/io/functions/iologindata_save_player.hpp @@ -16,7 +16,6 @@ class PropWriteStream; class IOLoginDataSave : public IOLoginData { public: static bool savePlayerFirst(const std::shared_ptr &player); - static bool savePlayerStash(const std::shared_ptr &player); static bool savePlayerSpells(const std::shared_ptr &player); static bool savePlayerKills(const std::shared_ptr &player); static bool savePlayerBestiarySystem(const std::shared_ptr &player); @@ -26,9 +25,7 @@ class IOLoginDataSave : public IOLoginData { static bool savePlayerInbox(const std::shared_ptr &player); static bool savePlayerPreyClass(const std::shared_ptr &player); static bool savePlayerTaskHuntingClass(const std::shared_ptr &player); - static bool savePlayerForgeHistory(const std::shared_ptr &player); static bool savePlayerBosstiary(const std::shared_ptr &player); - static bool savePlayerStorage(const std::shared_ptr &player); protected: using ItemBlockList = std::list>>; diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 3e8a05524e6..c29d3d81f44 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -11,6 +11,7 @@ #include "creatures/monsters/monsters.hpp" #include "creatures/players/player.hpp" +#include "creatures/players/components/player_storage.hpp" #include "game/game.hpp" #include "lib/di/container.hpp" #include "utils/tools.hpp" @@ -196,7 +197,7 @@ void IOBosstiary::addBosstiaryKill(const std::shared_ptr &player, const auto pointsForCurrentLevel = infoForCurrentRace[newBossLevel - 1].points; player->addBossPoints(pointsForCurrentLevel); - int32_t value = player->getStorageValue(STORAGEVALUE_PODIUM); + int32_t value = player->storage()->get(STORAGEVALUE_PODIUM); if (value != 1 && newBossLevel == 2) { auto returnValue = g_game().addItemStoreInbox(player, ITEM_PODIUM_OF_VIGOUR); if (!returnValue) { @@ -209,7 +210,7 @@ void IOBosstiary::addBosstiaryKill(const std::shared_ptr &player, const "Use it to display bosses for which you have reached at least the Expertise level."; player->sendTextMessage(MESSAGE_GAME_HIGHLIGHT, podiumMessage); - player->addStorageValue(STORAGEVALUE_PODIUM, 1); + player->storage()->add(STORAGEVALUE_PODIUM, 1); } } diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 0249d70a517..e721045a7f3 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -14,6 +14,9 @@ #include "io/functions/iologindata_save_player.hpp" #include "game/game.hpp" #include "creatures/monsters/monster.hpp" +#include "creatures/players/components/player_forge_history.hpp" +#include "creatures/players/components/player_stash.hpp" +#include "creatures/players/components/player_storage.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "lib/metrics/metrics.hpp" #include "enums/account_type.hpp" @@ -126,7 +129,7 @@ bool IOLoginData::loadPlayer(const std::shared_ptr &player, const DBResu IOLoginDataLoad::loadPlayerGuild(player, result); // stash load items - IOLoginDataLoad::loadPlayerStashItems(player, result); + player->stash()->load(); // bestiary charms IOLoginDataLoad::loadPlayerBestiaryCharms(player, result); @@ -147,7 +150,7 @@ bool IOLoginData::loadPlayer(const std::shared_ptr &player, const DBResu IOLoginDataLoad::loadPlayerInboxItems(player, result); // load storage map - IOLoginDataLoad::loadPlayerStorageMap(player, result); + player->storage()->load(); // load vip IOLoginDataLoad::loadPlayerVip(player, result); @@ -166,7 +169,7 @@ bool IOLoginData::loadPlayer(const std::shared_ptr &player, const DBResu } // load forge history - IOLoginDataLoad::loadPlayerForgeHistory(player, result); + player->stash()->load(); // load bosstiary IOLoginDataLoad::loadPlayerBosstiary(player, result); @@ -211,7 +214,7 @@ bool IOLoginData::savePlayerGuard(const std::shared_ptr &player) { throw DatabaseException("[" + std::string(__FUNCTION__) + "] - Failed to save player first: " + player->getName()); } - if (!IOLoginDataSave::savePlayerStash(player)) { + if (!player->stash()->save()) { throw DatabaseException("[IOLoginDataSave::savePlayerFirst] - Failed to save player stash: " + player->getName()); } @@ -251,7 +254,7 @@ bool IOLoginData::savePlayerGuard(const std::shared_ptr &player) { throw DatabaseException("[IOLoginDataSave::savePlayerTaskHuntingClass] - Failed to save player task hunting class: " + player->getName()); } - if (!IOLoginDataSave::savePlayerForgeHistory(player)) { + if (!player->forgeHistory()->save()) { throw DatabaseException("[IOLoginDataSave::savePlayerForgeHistory] - Failed to save player forge history: " + player->getName()); } @@ -263,7 +266,7 @@ bool IOLoginData::savePlayerGuard(const std::shared_ptr &player) { throw DatabaseException("[PlayerWheel::saveDBPlayerSlotPointsOnLogout] - Failed to save player wheel info: " + player->getName()); } - if (!IOLoginDataSave::savePlayerStorage(player)) { + if (!player->storage()->save()) { throw DatabaseException("[IOLoginDataSave::savePlayerStorage] - Failed to save player storage: " + player->getName()); } diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index c4c2faa8cee..2788a63551d 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -17,6 +17,7 @@ #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" +#include "creatures/players/components/player_storage.hpp" #include "creatures/players/player.hpp" #include "creatures/players/vip/player_vip.hpp" #include "creatures/players/vocations/vocation.hpp" @@ -1734,7 +1735,7 @@ int PlayerFunctions::luaPlayerGetStorageValue(lua_State* L) { } const uint32_t key = getNumber(L, 2); - lua_pushnumber(L, player->getStorageValue(key)); + lua_pushnumber(L, player->storage()->get(key)); return 1; } @@ -1757,7 +1758,7 @@ int PlayerFunctions::luaPlayerSetStorageValue(lua_State* L) { } if (player) { - player->addStorageValue(key, value); + player->storage()->add(key, value); pushBoolean(L, true); } else { lua_pushnil(L); @@ -1776,7 +1777,7 @@ int PlayerFunctions::luaPlayerGetStorageValueByName(lua_State* L) { g_logger().warn("The function 'player:getStorageValueByName' is deprecated and will be removed in future versions, please use KV system"); const auto name = getString(L, 2); - lua_pushnumber(L, player->getStorageValueByName(name)); + lua_pushnumber(L, player->storage()->get(name)); return 1; } @@ -1793,7 +1794,7 @@ int PlayerFunctions::luaPlayerSetStorageValueByName(lua_State* L) { const auto storageName = getString(L, 2); const int32_t value = getNumber(L, 3); - player->addStorageValueByName(storageName, value); + player->storage()->add(storageName, value); pushBoolean(L, true); return 1; } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 99c8a960677..f5c07c25a5a 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -30,6 +30,8 @@ #include "creatures/players/management/waitlist.hpp" #include "creatures/players/player.hpp" #include "creatures/players/vip/player_vip.hpp" +#include "creatures/players/components/player_forge_history.hpp" +#include "creatures/players/components/player_storage.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "enums/player_icons.hpp" #include "game/game.hpp" @@ -5676,7 +5678,7 @@ void ProtocolGame::sendForgeResult(ForgeAction_t actionType, uint16_t leftItemId void ProtocolGame::sendForgeHistory(uint8_t page) { page = page + 1; - auto historyVector = player->getForgeHistory(); + auto historyVector = player->forgeHistory()->get(); auto historyVectorLen = getVectorIterationIncreaseCount(historyVector); uint16_t lastPage = (1 < std::floor((historyVectorLen - 1) / 9) + 1) ? static_cast(std::floor((historyVectorLen - 1) / 9) + 1) : 1; @@ -8312,7 +8314,7 @@ void ProtocolGame::AddHiddenShopItem(NetworkMessage &msg) { void ProtocolGame::AddShopItem(NetworkMessage &msg, const ShopBlock &shopBlock) { // Sends the item information empty if the player doesn't have the storage to buy/sell a certain item - if (shopBlock.itemStorageKey != 0 && player->getStorageValue(shopBlock.itemStorageKey) < shopBlock.itemStorageValue) { + if (shopBlock.itemStorageKey != 0 && player->storage()->get(shopBlock.itemStorageKey) < shopBlock.itemStorageValue) { AddHiddenShopItem(msg); return; }