From b8f4090e6c19d9a813e6f9ecbe7e06d2835aacb0 Mon Sep 17 00:00:00 2001 From: "Leilani A." <168607226+kaleohanopahala@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:15:37 -0300 Subject: [PATCH 01/18] fix: thorgrin callbacks (#3202) --- data-otservbr-global/npc/thorgrin.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data-otservbr-global/npc/thorgrin.lua b/data-otservbr-global/npc/thorgrin.lua index 976c404af57..48eba7866bd 100644 --- a/data-otservbr-global/npc/thorgrin.lua +++ b/data-otservbr-global/npc/thorgrin.lua @@ -52,15 +52,15 @@ npcType.onCloseChannel = function(npc, creature) end -- Travel -local function addTravelKeyword(keyword, cost, destination) - local travelKeyword = keywordHandler:addKeyword({ keyword }, StdModule.say, { npcHandler = npcHandler, text = "Do you seek a ride to " .. keyword:titleCase() .. " for |TRAVELCOST|?", cost = cost, discount = "postman" }) - travelKeyword:addChildKeyword({ "yes" }, StdModule.travel, { npcHandler = npcHandler, premium = false, cost = cost, discount = "postman", destination = destination }) - travelKeyword:addChildKeyword({ "no" }, StdModule.say, { npcHandler = npcHandler, text = "Then not.", reset = true }) +local function addTravelKeyword(keyword, cost, destination, text, action, discount) + local travelKeyword = keywordHandler:addKeyword({ keyword }, StdModule.say, { npcHandler = npcHandler, text = text or "Do you seek a ride to " .. keyword:titleCase() .. " for |TRAVELCOST|?", cost = cost, discount = discount or "postman" }) + travelKeyword:addChildKeyword({ "yes" }, StdModule.travel, { npcHandler = npcHandler, premium = false, cost = cost, discount = discount or "postman", destination = destination, text = "Full steam ahead!" }, nil, action) + travelKeyword:addChildKeyword({ "no" }, StdModule.say, { npcHandler = npcHandler, text = "Then not", reset = true }) end addTravelKeyword("kazordoon", 210, Position(32659, 31957, 15)) addTravelKeyword("cormaya", 110, Position(33310, 31988, 15)) -addTravelKeyword("gnomprona", { "Would you like to travel to Gnomprona for |TRAVELCOST|?", "Full steam ahead!", "Then not." }, 200, "postman", Position(33516, 32856, 14)) +addTravelKeyword("gnomprona", 200, Position(33516, 32856, 14), "Would you like to travel to Gnomprona for |TRAVELCOST|?", nil, "postman") keywordHandler:addKeyword({ "passage" }, StdModule.say, { npcHandler = npcHandler, text = "Do you want me take you to {Cormaya}, {Kazordoon} or {Gnomprona}?" }) From 15e3f1dec2de3ff471b80496922a31e61699b7fb Mon Sep 17 00:00:00 2001 From: Jean Carlo de Souza Date: Thu, 2 Jan 2025 14:19:12 -0300 Subject: [PATCH 02/18] fix: prevent player corpses from being removed during tile cleaning (#3205) --- data/scripts/movements/closing_door.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/scripts/movements/closing_door.lua b/data/scripts/movements/closing_door.lua index 3a63407267e..1f977deb403 100644 --- a/data/scripts/movements/closing_door.lua +++ b/data/scripts/movements/closing_door.lua @@ -104,7 +104,7 @@ function closingDoor.onStepOut(creature, item, position, fromPosition) while tileItem and i < tileCount do tileItem = tile:getThing(i) - if tileItem and tileItem:getUniqueId() ~= item.uid and tileItem:getType():isMovable() then + if tileItem and tileItem:getUniqueId() ~= item.uid and tileItem:getType():isMovable() and not isCorpse(tileItem:getUniqueId()) then tileItem:remove() else i = i + 1 From 8e6a4fd16cd8208a48ad441db4b2a2a744650134 Mon Sep 17 00:00:00 2001 From: Jean Carlo de Souza Date: Thu, 2 Jan 2025 14:19:23 -0300 Subject: [PATCH 03/18] fix: preserve decimal precision for skill percentage calculation (#3207) --- src/creatures/players/player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index bfbc8148d0a..979f5d9a74d 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -885,7 +885,7 @@ void Player::addSkillAdvance(skills_t skill, uint64_t count) { skills[skill].tries += count; - uint32_t newPercent; + double_t newPercent; if (nextReqTries > currReqTries) { newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries); } else { From 4a92d84d629d54df3184500263f32943681cf69f Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Thu, 2 Jan 2025 16:51:12 -0300 Subject: [PATCH 04/18] fix: remove unnecessary function for data-canary (#2973) --- data-otservbr-global/lib/functions/load.lua | 1 + .../lib/functions/players.lua | 76 +++++++++++++++++ data-otservbr-global/lib/lib.lua | 3 + data-otservbr-global/npc/thorgrin.lua | 6 +- data/libs/functions/player.lua | 85 ------------------- 5 files changed, 83 insertions(+), 88 deletions(-) create mode 100644 data-otservbr-global/lib/functions/load.lua create mode 100644 data-otservbr-global/lib/functions/players.lua diff --git a/data-otservbr-global/lib/functions/load.lua b/data-otservbr-global/lib/functions/load.lua new file mode 100644 index 00000000000..9862b00d2d8 --- /dev/null +++ b/data-otservbr-global/lib/functions/load.lua @@ -0,0 +1 @@ +dofile(DATA_DIRECTORY .. "/lib/functions/players.lua") diff --git a/data-otservbr-global/lib/functions/players.lua b/data-otservbr-global/lib/functions/players.lua new file mode 100644 index 00000000000..45fec4c6653 --- /dev/null +++ b/data-otservbr-global/lib/functions/players.lua @@ -0,0 +1,76 @@ +function Player.getCookiesDelivered(self) + local storage, amount = + { + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.SimonTheBeggar, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Markwin, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Ariella, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Hairycles, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Djinn, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.AvarTar, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.OrcKing, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Lorbas, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Wyda, + Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Hjaern, + }, 0 + for i = 1, #storage do + if self:getStorageValue(storage[i]) == 1 then + amount = amount + 1 + end + end + return amount +end + +function Player.checkGnomeRank(self) + local points = self:getStorageValue(Storage.Quest.U9_60.BigfootsBurden.Rank) + local questProgress = self:getStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine) + if points >= 30 and points < 120 then + if questProgress <= 25 then + self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 26) + self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + self:addAchievement("Gnome Little Helper") + end + elseif points >= 120 and points < 480 then + if questProgress <= 26 then + self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 27) + self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + self:addAchievement("Gnome Little Helper") + self:addAchievement("Gnome Friend") + end + elseif points >= 480 and points < 1440 then + if questProgress <= 27 then + self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 28) + self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + self:addAchievement("Gnome Little Helper") + self:addAchievement("Gnome Friend") + self:addAchievement("Gnomelike") + end + elseif points >= 1440 then + if questProgress <= 29 then + self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 30) + self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + self:addAchievement("Gnome Little Helper") + self:addAchievement("Gnome Friend") + self:addAchievement("Gnomelike") + self:addAchievement("Honorary Gnome") + end + end + return true +end + +function Player.addFamePoint(self) + local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) + local current = math.max(0, points) + self:setStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points, current + 1) + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have received a fame point.") +end + +function Player.getFamePoints(self) + local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) + return math.max(0, points) +end + +function Player.removeFamePoints(self, amount) + local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) + local current = math.max(0, points) + self:setStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points, current - amount) +end diff --git a/data-otservbr-global/lib/lib.lua b/data-otservbr-global/lib/lib.lua index 2e9ead889eb..e6ee85db8b4 100644 --- a/data-otservbr-global/lib/lib.lua +++ b/data-otservbr-global/lib/lib.lua @@ -9,3 +9,6 @@ dofile(DATA_DIRECTORY .. "/lib/quests/quest.lua") -- Tables library dofile(DATA_DIRECTORY .. "/lib/tables/load.lua") + +-- Functions library +dofile(DATA_DIRECTORY .. "/lib/functions/load.lua") diff --git a/data-otservbr-global/npc/thorgrin.lua b/data-otservbr-global/npc/thorgrin.lua index 48eba7866bd..febc93e0c2d 100644 --- a/data-otservbr-global/npc/thorgrin.lua +++ b/data-otservbr-global/npc/thorgrin.lua @@ -53,9 +53,9 @@ end -- Travel local function addTravelKeyword(keyword, cost, destination, text, action, discount) - local travelKeyword = keywordHandler:addKeyword({ keyword }, StdModule.say, { npcHandler = npcHandler, text = text or "Do you seek a ride to " .. keyword:titleCase() .. " for |TRAVELCOST|?", cost = cost, discount = discount or "postman" }) - travelKeyword:addChildKeyword({ "yes" }, StdModule.travel, { npcHandler = npcHandler, premium = false, cost = cost, discount = discount or "postman", destination = destination, text = "Full steam ahead!" }, nil, action) - travelKeyword:addChildKeyword({ "no" }, StdModule.say, { npcHandler = npcHandler, text = "Then not", reset = true }) + local travelKeyword = keywordHandler:addKeyword({ keyword }, StdModule.say, { npcHandler = npcHandler, text = text or "Do you seek a ride to " .. keyword:titleCase() .. " for |TRAVELCOST|?", cost = cost, discount = discount or "postman" }) + travelKeyword:addChildKeyword({ "yes" }, StdModule.travel, { npcHandler = npcHandler, premium = false, cost = cost, discount = discount or "postman", destination = destination, text = "Full steam ahead!" }, nil, action) + travelKeyword:addChildKeyword({ "no" }, StdModule.say, { npcHandler = npcHandler, text = "Then not", reset = true }) end addTravelKeyword("kazordoon", 210, Position(32659, 31957, 15)) diff --git a/data/libs/functions/player.lua b/data/libs/functions/player.lua index 0350310e397..8dae1cb7fe5 100644 --- a/data/libs/functions/player.lua +++ b/data/libs/functions/player.lua @@ -90,91 +90,6 @@ function Player.addManaSpent(...) end -- Functions From OTServBR-Global -function Player.getCookiesDelivered(self) - if not IsRunningGlobalDatapack() then - return true - end - - local storage, amount = - { - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.SimonTheBeggar, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Markwin, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Ariella, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Hairycles, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Djinn, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.AvarTar, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.OrcKing, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Lorbas, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Wyda, - Storage.Quest.U8_1.WhatAFoolishQuest.CookieDelivery.Hjaern, - }, 0 - for i = 1, #storage do - if self:getStorageValue(storage[i]) == 1 then - amount = amount + 1 - end - end - return amount -end - -function Player.checkGnomeRank(self) - if not IsRunningGlobalDatapack() then - return true - end - - local points = self:getStorageValue(Storage.Quest.U9_60.BigfootsBurden.Rank) - local questProgress = self:getStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine) - if points >= 30 and points < 120 then - if questProgress <= 25 then - self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 26) - self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - self:addAchievement("Gnome Little Helper") - end - elseif points >= 120 and points < 480 then - if questProgress <= 26 then - self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 27) - self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - self:addAchievement("Gnome Little Helper") - self:addAchievement("Gnome Friend") - end - elseif points >= 480 and points < 1440 then - if questProgress <= 27 then - self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 28) - self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - self:addAchievement("Gnome Little Helper") - self:addAchievement("Gnome Friend") - self:addAchievement("Gnomelike") - end - elseif points >= 1440 then - if questProgress <= 29 then - self:setStorageValue(Storage.Quest.U9_60.BigfootsBurden.QuestLine, 30) - self:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - self:addAchievement("Gnome Little Helper") - self:addAchievement("Gnome Friend") - self:addAchievement("Gnomelike") - self:addAchievement("Honorary Gnome") - end - end - return true -end - -function Player.addFamePoint(self) - local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) - local current = math.max(0, points) - self:setStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points, current + 1) - self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have received a fame point.") -end - -function Player.getFamePoints(self) - local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) - return math.max(0, points) -end - -function Player.removeFamePoints(self, amount) - local points = self:getStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points) - local current = math.max(0, points) - self:setStorageValue(Storage.Quest.U10_20.SpikeTaskQuest.Constants.Spike_Fame_Points, current - amount) -end - function Player.depositMoney(self, amount) return Bank.deposit(self, amount) end From e5fb63ca1172fb9463ec021af8c404fe0d6fbf0e Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 2 Jan 2025 17:22:56 -0300 Subject: [PATCH 05/18] feat: create database backup on server shutdown (#3069) This update introduces a refined automatic database backup feature during the server shutdown process. The main improvements include: 1. Automatic Compression: The database backup is now always compressed using gzip, reducing disk space usage. 2. Backup Management: The system organizes backup files into folders named by date and automatically deletes backups older than 7 days. This ensures that the backup storage remains manageable over time without manual intervention. The motivation behind these changes is to create a more efficient and reliable way of managing database backups, ensuring data safety while optimizing storage space usage. The feature can be highly useful for production servers, as it creates backups during shutdown and maintains them efficiently by automatically removing old backups. --- .github/workflows/build-ubuntu.yml | 10 +-- .gitignore | 3 + config.lua.dist | 1 + src/canary_server.cpp | 1 + src/config/config_enums.hpp | 1 + src/config/configmanager.cpp | 1 + src/database/database.cpp | 97 ++++++++++++++++++++++++++++++ src/database/database.hpp | 17 ++++++ 8 files changed, 127 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index 6a252630f48..692bafc331c 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -52,12 +52,14 @@ jobs: run: > sudo apt-get update && sudo apt-get install ccache linux-headers-"$(uname -r)" - - name: Switch to gcc-12 on Ubuntu 22.04 + - name: Switch to gcc-13 on Ubuntu 22.04 if: matrix.os == 'ubuntu-22.04' run: | - sudo apt install gcc-12 g++-12 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 --slave /usr/bin/g++ g++ /usr/bin/g++-12 --slave /usr/bin/gcov gcov /usr/bin/gcov-12 - sudo update-alternatives --set gcc /usr/bin/gcc-12 + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt-get update + sudo apt install gcc-13 g++-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-12 + sudo update-alternatives --set gcc /usr/bin/gcc-13 - name: Switch to gcc-14 on Ubuntu 24.04 if: matrix.os == 'ubuntu-24.04' diff --git a/.gitignore b/.gitignore index a73a8c3022c..4bd08dd8908 100644 --- a/.gitignore +++ b/.gitignore @@ -395,5 +395,8 @@ canary.old # VCPKG vcpkg_installed +# DB Backups +database_backup + # CLION cmake-build-* diff --git a/config.lua.dist b/config.lua.dist index 7c891c84b5d..60c4b770063 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -401,6 +401,7 @@ mysqlHost = "127.0.0.1" mysqlUser = "root" mysqlPass = "root" mysqlDatabase = "otservbr-global" +mysqlDatabaseBackup = false mysqlPort = 3306 mysqlSock = "" passwordType = "sha1" diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 8dfbcfc954c..492028093ed 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -390,6 +390,7 @@ void CanaryServer::modulesLoadHelper(bool loaded, std::string moduleName) { } void CanaryServer::shutdown() { + g_database().createDatabaseBackup(true); g_dispatcher().shutdown(); g_metrics().shutdown(); inject().shutdown(); diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 65e585159e8..d180cc668c2 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -167,6 +167,7 @@ enum ConfigKey_t : uint16_t { MONTH_KILLS_TO_RED, MULTIPLIER_ATTACKONFIST, MYSQL_DB, + MYSQL_DB_BACKUP, MYSQL_HOST, MYSQL_PASS, MYSQL_SOCK, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1034a28be3e..5ef93d5461b 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -71,6 +71,7 @@ bool ConfigManager::load() { loadStringConfig(L, MAP_DOWNLOAD_URL, "mapDownloadUrl", ""); loadStringConfig(L, MAP_NAME, "mapName", "canary"); loadStringConfig(L, MYSQL_DB, "mysqlDatabase", "canary"); + loadBoolConfig(L, MYSQL_DB_BACKUP, "mysqlDatabaseBackup", false); loadStringConfig(L, MYSQL_HOST, "mysqlHost", "127.0.0.1"); loadStringConfig(L, MYSQL_PASS, "mysqlPass", ""); loadStringConfig(L, MYSQL_SOCK, "mysqlSock", ""); diff --git a/src/database/database.cpp b/src/database/database.cpp index fcb7371708d..a6e732694b1 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -12,6 +12,7 @@ #include "config/configmanager.hpp" #include "lib/di/container.hpp" #include "lib/metrics/metrics.hpp" +#include "utils/tools.hpp" Database::~Database() { if (handle != nullptr) { @@ -60,6 +61,102 @@ bool Database::connect(const std::string* host, const std::string* user, const s return true; } +void Database::createDatabaseBackup(bool compress) const { + if (!g_configManager().getBoolean(MYSQL_DB_BACKUP)) { + return; + } + + // Get current time for formatting + auto now = std::chrono::system_clock::now(); + std::time_t now_c = std::chrono::system_clock::to_time_t(now); + std::string formattedDate = fmt::format("{:%Y-%m-%d}", fmt::localtime(now_c)); + std::string formattedTime = fmt::format("{:%H-%M-%S}", fmt::localtime(now_c)); + + // Create a backup directory based on the current date + std::string backupDir = fmt::format("database_backup/{}/", formattedDate); + std::filesystem::create_directories(backupDir); + std::string backupFileName = fmt::format("{}backup_{}.sql", backupDir, formattedTime); + + // Create a temporary configuration file for MySQL credentials + std::string tempConfigFile = "database_backup.cnf"; + std::ofstream configFile(tempConfigFile); + if (configFile.is_open()) { + configFile << "[client]\n"; + configFile << "user=" << g_configManager().getString(MYSQL_USER) << "\n"; + configFile << "password=" << g_configManager().getString(MYSQL_PASS) << "\n"; + configFile << "host=" << g_configManager().getString(MYSQL_HOST) << "\n"; + configFile << "port=" << g_configManager().getNumber(SQL_PORT) << "\n"; + configFile.close(); + } else { + g_logger().error("Failed to create temporary MySQL configuration file."); + return; + } + + // Execute mysqldump command to create backup file + std::string command = fmt::format( + "mysqldump --defaults-extra-file={} {} > {}", + tempConfigFile, g_configManager().getString(MYSQL_DB), backupFileName + ); + + int result = std::system(command.c_str()); + std::filesystem::remove(tempConfigFile); + + if (result != 0) { + g_logger().error("Failed to create database backup using mysqldump."); + return; + } + + // Compress the backup file if requested + std::string compressedFileName; + compressedFileName = backupFileName + ".gz"; + gzFile gzFile = gzopen(compressedFileName.c_str(), "wb9"); + if (!gzFile) { + g_logger().error("Failed to open gzip file for compression."); + return; + } + + std::ifstream backupFile(backupFileName, std::ios::binary); + if (!backupFile.is_open()) { + g_logger().error("Failed to open backup file for compression: {}", backupFileName); + gzclose(gzFile); + return; + } + + std::string buffer(8192, '\0'); + while (backupFile.read(&buffer[0], buffer.size()) || backupFile.gcount() > 0) { + gzwrite(gzFile, buffer.data(), backupFile.gcount()); + } + + backupFile.close(); + gzclose(gzFile); + std::filesystem::remove(backupFileName); + + g_logger().info("Database backup successfully compressed to: {}", compressedFileName); + + // Delete backups older than 7 days + auto nowTime = std::chrono::system_clock::now(); + auto sevenDaysAgo = nowTime - std::chrono::hours(7 * 24); // 7 days in hours + for (const auto &entry : std::filesystem::directory_iterator("database_backup")) { + if (entry.is_directory()) { + try { + for (const auto &file : std::filesystem::directory_iterator(entry)) { + if (file.path().extension() == ".gz") { + auto fileTime = std::filesystem::last_write_time(file); + auto fileTimeSystemClock = std::chrono::clock_cast(fileTime); + + if (fileTimeSystemClock < sevenDaysAgo) { + std::filesystem::remove(file); + g_logger().info("Deleted old backup file: {}", file.path().string()); + } + } + } + } catch (const std::filesystem::filesystem_error &e) { + g_logger().error("Failed to check or delete files in backup directory: {}. Error: {}", entry.path().string(), e.what()); + } + } + } +} + bool Database::beginTransaction() { if (!executeQuery("BEGIN")) { return false; diff --git a/src/database/database.hpp b/src/database/database.hpp index 69c47d324ad..3566425a5cd 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -37,6 +37,23 @@ class Database { bool connect(const std::string* host, const std::string* user, const std::string* password, const std::string* database, uint32_t port, const std::string* sock); + /** + * @brief Creates a backup of the database. + * + * This function generates a backup of the database, with options for compression. + * The backup can be triggered periodically or during specific events like server loading. + * + * The backup operation will only execute if the configuration option `MYSQL_DB_BACKUP` + * is set to true in the `config.lua` file. If this configuration is disabled, the function + * will return without performing any action. + * + * @param compress Indicates whether the backup should be compressed. + * - If `compress` is true, the backup is created during an interval-based save, which occurs every 2 hours. + * This helps prevent excessive growth in the number of backup files. + * - If `compress` is false, the backup is created during the global save, which is triggered once a day when the server loads. + */ + void createDatabaseBackup(bool compress) const; + bool retryQuery(std::string_view query, int retries); bool executeQuery(std::string_view query); From 09024a755c13e9fac58cfd0ce5ca10a664857add Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 20:56:18 -0300 Subject: [PATCH 06/18] improve: boosted boss selection (#3208) --- src/io/io_bosstiary.cpp | 118 +++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 3e8a05524e6..57a9a85e294 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -20,46 +20,46 @@ IOBosstiary &IOBosstiary::getInstance() { } void IOBosstiary::loadBoostedBoss() { - Database &database = Database::getInstance(); - std::ostringstream query; - query << "SELECT * FROM `boosted_boss`"; - DBResult_ptr result = database.storeQuery(query.str()); - if (!result) { - g_logger().error("[{}] Failed to detect boosted boss database. (CODE 01)", __FUNCTION__); - return; - } - - auto date = result->getNumber("date"); + std::string query = R"SQL( + SELECT `date`, `boostname`, `raceid`, `looktypeEx`, `looktype`, + `lookfeet`, `looklegs`, `lookhead`, `lookbody`, + `lookaddons`, `lookmount` + FROM `boosted_boss` + )SQL"; + + DBResult_ptr result = g_database().storeQuery(query); auto timeNow = getTimeNow(); auto time = localtime(&timeNow); auto today = time->tm_mday; - auto bossMap = getBosstiaryMap(); + const auto &bossMap = getBosstiaryMap(); if (bossMap.size() <= 1) { g_logger().error("[{}] It is not possible to create a boosted boss with only one registered boss. (CODE 02)", __FUNCTION__); return; } - std::string bossName; - uint16_t bossId = 0; - if (date == today) { - bossName = result->getString("boostname"); - bossId = result->getNumber("raceid"); - setBossBoostedName(bossName); - setBossBoostedId(bossId); - g_logger().info("Boosted boss: {}", bossName); - return; + if (!result) { + g_logger().warn("[{}] No boosted boss found in g_database(). A new one will be selected.", __FUNCTION__); + } else { + auto date = result->getNumber("date"); + if (date == today) { + std::string bossName = result->getString("boostname"); + uint16_t bossId = result->getNumber("raceid"); + setBossBoostedName(bossName); + setBossBoostedId(bossId); + g_logger().info("Boosted boss: {}", bossName); + return; + } } // Filter only archfoe bosses - std::map bossInfo; - for (auto [infoBossRaceId, infoBossName] : bossMap) { - const auto mType = getMonsterTypeByBossRaceId(infoBossRaceId); + std::vector> bossInfo; + for (const auto &[infoBossRaceId, infoBossName] : bossMap) { + const auto &mType = getMonsterTypeByBossRaceId(infoBossRaceId); if (!mType || mType->info.bosstiaryRace != BosstiaryRarity_t::RARITY_ARCHFOE) { continue; } - - bossInfo.try_emplace(infoBossRaceId, infoBossName); + bossInfo.emplace_back(infoBossRaceId, infoBossName); } // Check if not have archfoe registered boss @@ -68,55 +68,39 @@ void IOBosstiary::loadBoostedBoss() { return; } - auto oldBossRace = result->getNumber("raceid"); - while (true) { - uint32_t randomIndex = uniform_random(0, static_cast(bossInfo.size())); - auto it = std::next(bossInfo.begin(), randomIndex); - if (it == bossInfo.end()) { - break; - } - - const auto &[randomBossId, randomBossName] = *it; - if (randomBossId == oldBossRace) { - continue; - } - - bossName = randomBossName; - bossId = randomBossId; - break; + const auto &[randomBossId, randomBossName] = bossInfo[uniform_random(0, static_cast(bossInfo.size() - 1))]; + std::string bossName = randomBossName; + uint16_t bossId = randomBossId; + + query = fmt::format( + "UPDATE `boosted_boss` SET `date` = '{}', `boostname` = {}, ", + today, g_database().escapeString(bossName) + ); + if (const auto &bossType = getMonsterTypeByBossRaceId(bossId); bossType) { + query += fmt::format( + "`looktypeEx` = {}, `looktype` = {}, `lookfeet` = {}, `looklegs` = {}, " + "`lookhead` = {}, `lookbody` = {}, `lookaddons` = {}, `lookmount` = {}, ", + bossType->info.outfit.lookTypeEx, bossType->info.outfit.lookType, + bossType->info.outfit.lookFeet, bossType->info.outfit.lookLegs, + bossType->info.outfit.lookHead, bossType->info.outfit.lookBody, + bossType->info.outfit.lookAddons, bossType->info.outfit.lookMount + ); } + query += fmt::format("`raceid` = {}", bossId); - query.str(std::string()); - query << "UPDATE `boosted_boss` SET "; - query << "`date` = '" << today << "',"; - query << "`boostname` = " << database.escapeString(bossName) << ","; - if (const auto bossType = getMonsterTypeByBossRaceId(bossId); - bossType) { - query << "`looktypeEx` = " << static_cast(bossType->info.outfit.lookTypeEx) << ","; - query << "`looktype` = " << static_cast(bossType->info.outfit.lookType) << ","; - query << "`lookfeet` = " << static_cast(bossType->info.outfit.lookFeet) << ","; - query << "`looklegs` = " << static_cast(bossType->info.outfit.lookLegs) << ","; - query << "`lookhead` = " << static_cast(bossType->info.outfit.lookHead) << ","; - query << "`lookbody` = " << static_cast(bossType->info.outfit.lookBody) << ","; - query << "`lookaddons` = " << static_cast(bossType->info.outfit.lookAddons) << ","; - query << "`lookmount` = " << static_cast(bossType->info.outfit.lookMount) << ","; - } - query << "`raceid` = '" << bossId << "'"; - if (!database.executeQuery(query.str())) { - g_logger().error("[{}] Failed to detect boosted boss database. (CODE 03)", __FUNCTION__); + if (!g_database().executeQuery(query)) { + g_logger().error("[{}] Failed to update boosted boss in g_database(). (CODE 03)", __FUNCTION__); return; } - query.str(std::string()); - query << "UPDATE `player_bosstiary` SET `bossIdSlotOne` = 0 WHERE `bossIdSlotOne` = " << bossId; - if (!database.executeQuery(query.str())) { - g_logger().error("[{}] Failed to reset players selected boss slot 1. (CODE 03)", __FUNCTION__); + query = fmt::format("UPDATE `player_bosstiary` SET `bossIdSlotOne` = 0 WHERE `bossIdSlotOne` = {}", bossId); + if (!g_database().executeQuery(query)) { + g_logger().error("[{}] Failed to reset players' selected boss slot 1. (CODE 03)", __FUNCTION__); } - query.str(std::string()); - query << "UPDATE `player_bosstiary` SET `bossIdSlotTwo` = 0 WHERE `bossIdSlotTwo` = " << bossId; - if (!database.executeQuery(query.str())) { - g_logger().error("[{}] Failed to reset players selected boss slot 1. (CODE 03)", __FUNCTION__); + query = fmt::format("UPDATE `player_bosstiary` SET `bossIdSlotTwo` = 0 WHERE `bossIdSlotTwo` = {}", bossId); + if (!g_database().executeQuery(query)) { + g_logger().error("[{}] Failed to reset players' selected boss slot 2. (CODE 03)", __FUNCTION__); } setBossBoostedName(bossName); From e7c15913401b27b62016892278886df2f7be6737 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 20:58:26 -0300 Subject: [PATCH 07/18] perf: npc/monster storage with vector indexing (#3209) This modifies the internal storage of NPCs and monsters in the Game class from std::unordered_map to a combination of std::vector and std::unordered_map indexes for improved lookup efficiency and better memory locality. Motivation Previously, every time a search was performed for an NPC or a monster, the unordered map was iterated, and even though it provides average O(1) complexity for lookups, the actual cost becomes significant when handling a large dataset (e.g., 80,000 monsters). This cost arises from the overhead of hashing and poor memory locality, as unordered maps store elements in a hash table. Switching to a vector with index-based lookup improves the following: 1. Memory locality: Vectors store elements contiguously in memory, improving cache efficiency when accessing elements sequentially or repeatedly. 2. Lookup efficiency: Using an unordered map as an index to the vector allows leveraging direct index-based access (constant time) to the actual data, combining the best of both structures. --- src/creatures/combat/condition.cpp | 4 + src/creatures/monsters/monster.cpp | 1 + src/creatures/monsters/monster.hpp | 5 + src/creatures/npcs/npc.cpp | 4 + src/creatures/npcs/npc.hpp | 2 + src/creatures/npcs/npcs.cpp | 3 + src/creatures/npcs/npcs.hpp | 4 +- src/game/game.cpp | 132 +++++++++++++----- src/game/game.hpp | 20 ++- .../monster/monster_type_functions.cpp | 24 ++-- 10 files changed, 146 insertions(+), 53 deletions(-) diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp index da90b03f65a..eac70f8ba74 100644 --- a/src/creatures/combat/condition.cpp +++ b/src/creatures/combat/condition.cpp @@ -1769,6 +1769,10 @@ bool ConditionDamage::getNextDamage(int32_t &damage) { } bool ConditionDamage::doDamage(const std::shared_ptr &creature, int32_t healthChange) const { + if (owner == 0) { + return false; + } + const auto &attacker = g_game().getPlayerByGUID(owner) ? g_game().getPlayerByGUID(owner)->getCreature() : g_game().getCreatureByID(owner); bool isPlayer = attacker && attacker->getPlayer(); if (creature->isSuppress(getType(), isPlayer)) { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 13b30321a21..845544d61e9 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -35,6 +35,7 @@ std::shared_ptr Monster::createMonster(const std::string &name) { } Monster::Monster(const std::shared_ptr &mType) : + m_lowerName(asLowerCaseString(mType->name)), nameDescription(asLowerCaseString(mType->nameDescription)), mType(mType) { defaultOutfit = mType->info.outfit; diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 6e44d8f36bf..3661eea9bef 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -49,6 +49,10 @@ class Monster final : public Creature { void setNameDescription(std::string_view nameDescription); std::string getDescription(int32_t) override; + const std::string &getLowerName() const { + return m_lowerName; + } + CreatureType_t getType() const override; const Position &getMasterPos() const; @@ -244,6 +248,7 @@ class Monster final : public Creature { ForgeClassifications_t monsterForgeClassification = ForgeClassifications_t::FORGE_NORMAL_MONSTER; std::string name; + std::string m_lowerName; std::string nameDescription; std::shared_ptr mType; diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index e2ab99a1dd4..cdcbd83c6b2 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -97,6 +97,10 @@ void Npc::setName(std::string newName) const { npcType->name = std::move(newName); } +const std::string &Npc::getLowerName() const { + return npcType->m_lowerName; +} + CreatureType_t Npc::getType() const { return CREATURETYPE_NPC; } diff --git a/src/creatures/npcs/npc.hpp b/src/creatures/npcs/npc.hpp index dc3e16961f1..47945b79e99 100644 --- a/src/creatures/npcs/npc.hpp +++ b/src/creatures/npcs/npc.hpp @@ -51,6 +51,8 @@ class Npc final : public Creature { void setName(std::string newName) const; + const std::string &getLowerName() const; + CreatureType_t getType() const override; const Position &getMasterPos() const; diff --git a/src/creatures/npcs/npcs.cpp b/src/creatures/npcs/npcs.cpp index 53ed757336f..9abc6be6efd 100644 --- a/src/creatures/npcs/npcs.cpp +++ b/src/creatures/npcs/npcs.cpp @@ -16,6 +16,9 @@ #include "lua/scripts/scripts.hpp" #include "lib/di/container.hpp" +NpcType::NpcType(const std::string &initName) : + name(initName), m_lowerName(asLowerCaseString(initName)), typeName(initName), nameDescription(initName) {}; + bool NpcType::canSpawn(const Position &pos) const { bool canSpawn = true; const bool isDay = g_game().gameIsDay(); diff --git a/src/creatures/npcs/npcs.hpp b/src/creatures/npcs/npcs.hpp index b91e1547c07..d587a17b9fa 100644 --- a/src/creatures/npcs/npcs.hpp +++ b/src/creatures/npcs/npcs.hpp @@ -77,14 +77,14 @@ class NpcType final : public SharedObject { public: NpcType() = default; - explicit NpcType(const std::string &initName) : - name(initName), typeName(initName), nameDescription(initName) {}; + explicit NpcType(const std::string &initName); // non-copyable NpcType(const NpcType &) = delete; NpcType &operator=(const NpcType &) = delete; std::string name; + std::string m_lowerName; std::string typeName; std::string nameDescription; NpcInfo info; diff --git a/src/game/game.cpp b/src/game/game.cpp index 7da1d87fb4a..54a666337d3 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -424,7 +424,7 @@ Game &Game::getInstance() { } void Game::resetMonsters() const { - for (const auto &[monsterId, monster] : getMonsters()) { + for (const auto &monster : getMonsters()) { monster->clearTargetList(); monster->clearFriendList(); } @@ -432,7 +432,7 @@ void Game::resetMonsters() const { void Game::resetNpcs() const { // Close shop window from all npcs and reset the shopPlayerSet - for (const auto &[npcId, npc] : getNpcs()) { + for (const auto &npc : getNpcs()) { npc->closeAllShopWindows(); npc->resetPlayerInteractions(); } @@ -954,11 +954,16 @@ std::shared_ptr Game::getMonsterByID(uint32_t id) { return nullptr; } - auto it = monsters.find(id); - if (it == monsters.end()) { + auto it = monstersIdIndex.find(id); + if (it == monstersIdIndex.end()) { return nullptr; } - return it->second; + + if (it->second >= monsters.size()) { + return nullptr; + } + + return monsters[it->second]; } std::shared_ptr Game::getNpcByID(uint32_t id) { @@ -966,11 +971,12 @@ std::shared_ptr Game::getNpcByID(uint32_t id) { return nullptr; } - auto it = npcs.find(id); - if (it == npcs.end()) { + auto it = npcsIdIndex.find(id); + if (it == npcsIdIndex.end()) { return nullptr; } - return it->second; + + return npcs[it->second]; } std::shared_ptr Game::getPlayerByID(uint32_t id, bool allowOffline /* = false */) { @@ -990,43 +996,41 @@ std::shared_ptr Game::getPlayerByID(uint32_t id, bool allowOffline /* = return tmpPlayer; } -std::shared_ptr Game::getCreatureByName(const std::string &s) { - if (s.empty()) { +std::shared_ptr Game::getCreatureByName(const std::string &creatureName) { + if (creatureName.empty()) { return nullptr; } - const std::string &lowerCaseName = asLowerCaseString(s); + const std::string &lowerCaseName = asLowerCaseString(creatureName); auto m_it = mappedPlayerNames.find(lowerCaseName); if (m_it != mappedPlayerNames.end()) { return m_it->second.lock(); } - for (const auto &it : npcs) { - if (lowerCaseName == asLowerCaseString(it.second->getName())) { - return it.second; - } + auto npcIterator = npcsNameIndex.find(lowerCaseName); + if (npcIterator != npcsNameIndex.end()) { + return npcs[npcIterator->second]; } - for (const auto &it : monsters) { - if (lowerCaseName == asLowerCaseString(it.second->getName())) { - return it.second; - } + auto monsterIterator = monstersNameIndex.find(lowerCaseName); + if (monsterIterator != monstersNameIndex.end()) { + return monsters[monsterIterator->second]; } return nullptr; } -std::shared_ptr Game::getNpcByName(const std::string &s) { - if (s.empty()) { +std::shared_ptr Game::getNpcByName(const std::string &npcName) { + if (npcName.empty()) { return nullptr; } - const char* npcName = s.c_str(); - for (const auto &it : npcs) { - if (strcasecmp(npcName, it.second->getName().c_str()) == 0) { - return it.second; - } + const std::string lowerCaseName = asLowerCaseString(npcName); + auto it = npcsNameIndex.find(lowerCaseName); + if (it != npcsNameIndex.end()) { + return npcs[it->second]; } + return nullptr; } @@ -3184,13 +3188,18 @@ ReturnValue Game::internalCollectManagedItems(const std::shared_ptr &pla ReturnValue Game::collectRewardChestItems(const std::shared_ptr &player, uint32_t maxMoveItems /* = 0*/) { // Check if have item on player reward chest - std::shared_ptr rewardChest = player->getRewardChest(); + const std::shared_ptr &rewardChest = player->getRewardChest(); if (rewardChest->empty()) { g_logger().debug("Reward chest is empty"); return RETURNVALUE_REWARDCHESTISEMPTY; } - auto rewardItemsVector = player->getRewardsFromContainer(rewardChest->getContainer()); + const auto &container = rewardChest->getContainer(); + if (!container) { + return RETURNVALUE_REWARDCHESTISEMPTY; + } + + auto rewardItemsVector = player->getRewardsFromContainer(container); auto rewardCount = rewardItemsVector.size(); uint32_t movedRewardItems = 0; std::string lootedItemsMessage; @@ -9931,19 +9940,72 @@ void Game::removePlayer(const std::shared_ptr &player) { } void Game::addNpc(const std::shared_ptr &npc) { - npcs[npc->getID()] = npc; + npcs.push_back(npc); + size_t index = npcs.size() - 1; + npcsNameIndex[npc->getLowerName()] = index; + npcsIdIndex[npc->getID()] = index; } void Game::removeNpc(const std::shared_ptr &npc) { - npcs.erase(npc->getID()); + if (!npc) { + return; + } + + auto npcId = npc->getID(); + const auto &npcLowerName = npc->getLowerName(); + auto it = npcsIdIndex.find(npcId); + if (it != npcsIdIndex.end()) { + size_t index = it->second; + npcsNameIndex.erase(npcLowerName); + npcsIdIndex.erase(npcId); + + if (index != npcs.size() - 1) { + std::swap(npcs[index], npcs.back()); + + const auto &movedNpc = npcs[index]; + npcsNameIndex[movedNpc->getLowerName()] = index; + npcsIdIndex[movedNpc->getID()] = index; + } + + npcs.pop_back(); + } } void Game::addMonster(const std::shared_ptr &monster) { - monsters[monster->getID()] = monster; + if (!monster) { + return; + } + + const auto &lowerName = monster->getLowerName(); + monsters.push_back(monster); + size_t index = monsters.size() - 1; + monstersNameIndex[lowerName] = index; + monstersIdIndex[monster->getID()] = index; } void Game::removeMonster(const std::shared_ptr &monster) { - monsters.erase(monster->getID()); + if (!monster) { + return; + } + + auto monsterId = monster->getID(); + const auto &monsterLowerName = monster->getLowerName(); + auto it = monstersIdIndex.find(monsterId); + if (it != monstersIdIndex.end()) { + size_t index = it->second; + monstersNameIndex.erase(monsterLowerName); + monstersIdIndex.erase(monsterId); + + if (index != monsters.size() - 1) { + std::swap(monsters[index], monsters.back()); + + const auto &movedMonster = monsters[index]; + monstersNameIndex[movedMonster->getLowerName()] = index; + monstersIdIndex[movedMonster->getID()] = index; + } + + monsters.pop_back(); + } } std::shared_ptr Game::getGuild(uint32_t id, bool allowOffline /* = flase */) const { @@ -10152,7 +10214,7 @@ uint32_t Game::makeFiendishMonster(uint32_t forgeableMonsterId /* = 0*/, bool cr forgeableMonsters.clear(); // If the forgeable monsters haven't been created // Then we'll create them so they don't return in the next if (forgeableMonsters.empty()) - for (const auto &[monsterId, monster] : monsters) { + for (const auto &monster : monsters) { auto monsterTile = monster->getTile(); if (!monster || !monsterTile) { continue; @@ -10325,7 +10387,7 @@ void Game::updateForgeableMonsters() { if (auto influencedLimit = g_configManager().getNumber(FORGE_INFLUENCED_CREATURES_LIMIT); forgeableMonsters.size() < influencedLimit) { forgeableMonsters.clear(); - for (const auto &[monsterId, monster] : monsters) { + for (const auto &monster : monsters) { const auto &monsterTile = monster->getTile(); if (!monsterTile) { continue; @@ -10541,7 +10603,7 @@ void Game::playerRewardChestCollect(uint32_t playerId, const Position &pos, uint } // Updates the parent of the reward chest and reward containers to avoid memory usage after cleaning - auto playerRewardChest = player->getRewardChest(); + const auto &playerRewardChest = player->getRewardChest(); if (playerRewardChest && playerRewardChest->empty()) { player->sendCancelMessage(RETURNVALUE_REWARDCHESTISEMPTY); return; diff --git a/src/game/game.hpp b/src/game/game.hpp index b16d1787d69..a59c7d13a70 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -522,10 +522,10 @@ class Game { const phmap::parallel_flat_hash_map> &getPlayers() const { return players; } - const std::map> &getMonsters() const { + const auto &getMonsters() const { return monsters; } - const std::map> &getNpcs() const { + const auto &getNpcs() const { return npcs; } @@ -539,8 +539,8 @@ class Game { void addNpc(const std::shared_ptr &npc); void removeNpc(const std::shared_ptr &npc); - void addMonster(const std::shared_ptr &npc); - void removeMonster(const std::shared_ptr &npc); + void addMonster(const std::shared_ptr &monster); + void removeMonster(const std::shared_ptr &monster); std::shared_ptr getGuild(uint32_t id, bool allowOffline = false) const; std::shared_ptr getGuildByName(const std::string &name, bool allowOffline = false) const; @@ -851,8 +851,16 @@ class Game { std::shared_ptr wildcardTree = nullptr; - std::map> npcs; - std::map> monsters; + std::vector> monsters; + // This works only for unique monsters (bosses, quest monsters, etc) + std::unordered_map monstersNameIndex; + std::unordered_map monstersIdIndex; + + std::vector> npcs; + // This works only for unique npcs (quest npcs, etc) + std::unordered_map npcsNameIndex; + std::unordered_map npcsIdIndex; + std::vector forgeableMonsters; std::map> teamFinderMap; // [leaderGUID] = TeamFinder* diff --git a/src/lua/functions/creatures/monster/monster_type_functions.cpp b/src/lua/functions/creatures/monster/monster_type_functions.cpp index 07ce44b626c..8b2f4a5f314 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.cpp @@ -1170,18 +1170,22 @@ int MonsterTypeFunctions::luaMonsterTypeGetCreatureEvents(lua_State* L) { int MonsterTypeFunctions::luaMonsterTypeRegisterEvent(lua_State* L) { // monsterType:registerEvent(name) const auto &monsterType = Lua::getUserdataShared(L, 1); - if (monsterType) { - const auto eventName = Lua::getString(L, 2); - monsterType->info.scripts.insert(eventName); - for (const auto &[_, monster] : g_game().getMonsters()) { - if (monster->getMonsterType() == monsterType) { - monster->registerCreatureEvent(eventName); - } - } - Lua::pushBoolean(L, true); - } else { + if (!monsterType) { lua_pushnil(L); + return 1; } + + const auto eventName = Lua::getString(L, 2); + monsterType->info.scripts.insert(eventName); + + for (const auto &monster : g_game().getMonsters()) { + const auto monsterTypeCompare = monster->getMonsterType(); + if (monsterTypeCompare == monsterType) { + monster->registerCreatureEvent(eventName); + } + } + + Lua::pushBoolean(L, true); return 1; } From 53015a3a11dbadb1547589037cfe521356dd278c Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 20:59:44 -0300 Subject: [PATCH 08/18] perf: change zone positions to phmap::flat_hash_set (#3210) This replaces `std::unordered_set` with `phmap::flat_hash_set` in the `Zone` class to improve performance by reducing hash collisions and optimizing memory usage. Motivation: The use of `std::unordered_set` for the `positions` attribute caused performance issues due to hash collisions, especially when managing large datasets or heavily clustered data. Collisions result in increased lookup, insertion, and deletion times as the hash table degrades into a linked list traversal for conflicting buckets. Switching to `phmap::flat_hash_set` provides several benefits: 1. Reduced hash collisions: The hashing strategy used by `phmap::flat_hash_set` significantly reduces the likelihood of collisions, improving lookup and insertion performance. 2. Improved memory locality: Unlike `std::unordered_set`, which uses separate allocations for each bucket, `flat_hash_set` uses a contiguous memory layout, enhancing cache efficiency and reducing overhead. 3. Faster operations: Benchmarks show that `flat_hash_set` outperforms `std::unordered_set` in scenarios with frequent insertions, lookups, and deletions due to its optimized design. --- src/game/zones/zone.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/zones/zone.hpp b/src/game/zones/zone.hpp index a3af961bd7f..e13652e8b32 100644 --- a/src/game/zones/zone.hpp +++ b/src/game/zones/zone.hpp @@ -216,7 +216,7 @@ class Zone { Position removeDestination = Position(); std::string name; std::string monsterVariant; - std::unordered_set positions; + phmap::flat_hash_set positions; uint32_t id = 0; // ID 0 is used in zones created dynamically from lua. The map editor uses IDs starting from 1 (automatically generated). weak::set itemsCache; From 2ca896ee665894f8253ef4ab9c8b835c592845c5 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 21:00:56 -0300 Subject: [PATCH 09/18] fix: concurrency crash in closeAllShopWindows caused by use-after-free (#3211) This fixes a concurrency crash in the `closeAllShopWindows` method. The issue occurred when the `shopPlayers` map was cleared while being iterated over, leading to a use-after-free scenario. The solution removes the use of references when iterating over `shopPlayers` keys, ensuring that the keys are copied instead of directly referencing the map. This change prevents crashes and improves stability in concurrent environments. --- src/creatures/npcs/npc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index cdcbd83c6b2..65bec559872 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -812,7 +812,7 @@ void Npc::removeShopPlayer(uint32_t playerGUID) { } void Npc::closeAllShopWindows() { - for (const auto &playerGUID : shopPlayers | std::views::keys) { + for (const auto playerGUID : shopPlayers | std::views::keys) { const auto &player = g_game().getPlayerByGUID(playerGUID); if (player) { player->closeShopWindow(); From 47780781797e766fb53df84a7a3524b150f29492 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 21:01:56 -0300 Subject: [PATCH 10/18] perf: optimize condition creation with ObjectPool (#3212) This introduces an ObjectPool for managing Condition objects, replacing frequent calls to std::make_shared with a lock-free pooling allocator. This optimization reduces memory allocation overhead, improves performance, and enhances thread safety in scenarios where Conditions are created and destroyed frequently, such as in onThink events or condition updates. By reusing objects from the pool instead of allocating and deallocating memory repeatedly, this change significantly reduces the strain on the memory management system and improves runtime efficiency. The pool is designed to handle up to 1024 objects per Condition type and supports safe, high-performance multithreaded operations. --- src/creatures/combat/condition.cpp | 33 ++++++------ src/utils/object_pool.hpp | 84 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 src/utils/object_pool.hpp diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp index eac70f8ba74..e5219126025 100644 --- a/src/creatures/combat/condition.cpp +++ b/src/creatures/combat/condition.cpp @@ -21,6 +21,7 @@ #include "creatures/creature.hpp" #include "creatures/players/player.hpp" #include "server/network/protocol/protocolgame.hpp" +#include "utils/object_pool.hpp" /** * Condition @@ -215,41 +216,41 @@ std::shared_ptr Condition::createCondition(ConditionId_t id, Conditio case CONDITION_DAZZLED: case CONDITION_CURSED: case CONDITION_BLEEDING: - return std::make_shared(id, type, buff, subId); + return ObjectPool::allocateShared(id, type, buff, subId); case CONDITION_HASTE: case CONDITION_PARALYZE: - return std::make_shared(id, type, ticks, buff, subId, param); + return ObjectPool::allocateShared(id, type, ticks, buff, subId, param); case CONDITION_INVISIBLE: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_OUTFIT: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_LIGHT: - return std::make_shared(id, type, ticks, buff, subId, param & 0xFF, (param & 0xFF00) >> 8); + return ObjectPool::allocateShared(id, type, ticks, buff, subId, param & 0xFF, (param & 0xFF00) >> 8); case CONDITION_REGENERATION: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_SOUL: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_ATTRIBUTES: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_SPELLCOOLDOWN: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_SPELLGROUPCOOLDOWN: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_MANASHIELD: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_FEARED: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); case CONDITION_ROOTED: case CONDITION_INFIGHT: @@ -261,11 +262,13 @@ std::shared_ptr Condition::createCondition(ConditionId_t id, Conditio case CONDITION_CHANNELMUTEDTICKS: case CONDITION_YELLTICKS: case CONDITION_PACIFIED: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); + case CONDITION_BAKRAGORE: - return std::make_shared(id, type, ticks, buff, subId, isPersistent); + return ObjectPool::allocateShared(id, type, ticks, buff, subId, isPersistent); + case CONDITION_GOSHNARTAINT: - return std::make_shared(id, type, ticks, buff, subId); + return ObjectPool::allocateShared(id, type, ticks, buff, subId); default: return nullptr; diff --git a/src/utils/object_pool.hpp b/src/utils/object_pool.hpp new file mode 100644 index 00000000000..c37d1d2efeb --- /dev/null +++ b/src/utils/object_pool.hpp @@ -0,0 +1,84 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 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/ + */ + +#pragma once + +#include "utils/lockfree.hpp" + +/** + * @brief A lock-free object pool for efficient memory allocation and reuse. + * + * This class provides an efficient mechanism for managing the allocation + * and deallocation of objects, reducing the overhead associated with + * frequent memory operations. It uses a lock-free structure to ensure + * thread safety and high performance in multithreaded environments. + * + * @tparam T The type of objects managed by the pool. + * @tparam CAPACITY The maximum number of objects that can be held in the pool. + */ +template +class ObjectPool { +public: + /** + * @brief The allocator type used for managing object memory. + */ + using Allocator = LockfreePoolingAllocator; + + /** + * @brief Allocates an object from the pool and returns it as a `std::shared_ptr`. + * + * The object is constructed in place using the provided arguments. + * The `std::shared_ptr` includes a custom deleter that returns the object + * to the pool when it is no longer needed. + * + * @tparam Args The types of the arguments used to construct the object.* @param args The arguments forwarded to the constructor of the object. + * @return A `std::shared_ptr` managing the allocated object, or `nullptr` if the pool is empty. + */ + template + static std::shared_ptr allocateShared(Args &&... args) { + T* obj = allocator.allocate(1); + if (obj) { + // Construct the object in place + std::construct_at(obj, std::forward(args)...); + + // Return a shared_ptr with a custom deleter + return std::shared_ptr(obj, [](T* ptr) { + std::destroy_at(ptr); // Destroy the object + allocator.deallocate(ptr, 1); // Return to the pool + }); + } + // Return nullptr if the pool is empty + return nullptr; + } + + static void clear() { + allocator.clear(); + } + + /** + * @brief Preallocates a specified number of objects in the pool. + * + * This method allows you to populate the pool with preallocated objects + * to improve performance by reducing the need for dynamic allocations at runtime. + * + * @param count The number of objects to preallocate. + */ + static void preallocate(size_t count) { + LockfreeFreeList::preallocate(count); + } + +private: + /** + * @brief The allocator instance used to manage object memory. + */ + static Allocator allocator; +}; + +template +typename ObjectPool::Allocator ObjectPool::allocator; From 34717832e2470fe17cf6f3820bbec8e330851ecd Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 21:02:37 -0300 Subject: [PATCH 11/18] fix: add null check for moveFunction to prevent crashes (#3213) This adds a null check for the moveFunction in the MoveEvent::fireAddRemItem method. Previously, the code accessed moveFunction directly without verifying its validity, which could lead to crashes if it was nullptr. The fix ensures stability by: Logging an error message when moveFunction is null. Returning early to prevent further execution with an invalid function pointer. This change improves the reliability of the function, particularly in scenarios where moveFunction might not be properly initialized. --- src/lua/creature/movement.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lua/creature/movement.cpp b/src/lua/creature/movement.cpp index 6804cdfa7e9..51daea8cf1e 100644 --- a/src/lua/creature/movement.cpp +++ b/src/lua/creature/movement.cpp @@ -808,6 +808,13 @@ uint32_t MoveEvent::fireAddRemItem(const std::shared_ptr &item, const std: if (isLoadedScriptId()) { return executeAddRemItem(item, fromTile, pos); } else { + if (!moveFunction) { + g_logger().error("[MoveEvent::fireAddRemItem - Item {} item on position: {}] " + "Move function is nullptr.", + item->getName(), pos.toString()); + return 0; + } + return moveFunction(item, fromTile, pos); } } @@ -840,6 +847,13 @@ uint32_t MoveEvent::fireAddRemItem(const std::shared_ptr &item, const Posi if (isLoadedScriptId()) { return executeAddRemItem(item, pos); } else { + if (!moveFunction) { + g_logger().error("[MoveEvent::fireAddRemItem - Item {} item on position: {}] " + "Move function is nullptr.", + item->getName(), pos.toString()); + return 0; + } + return moveFunction(item, nullptr, pos); } } @@ -849,9 +863,9 @@ bool MoveEvent::executeAddRemItem(const std::shared_ptr &item, const Posit // onRemoveItem(moveitem, pos) if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("[MoveEvent::executeAddRemItem - " - "Item {} item on tile x: {} y: {} z: {}] " + "Item {} item on position: {}] " "Call stack overflow. Too many lua script calls being nested.", - item->getName(), pos.getX(), pos.getY(), pos.getZ()); + item->getName(), pos.toString()); return false; } From b8ce7fc1afefbd1b16cd8830a620e4831ac2f688 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 4 Jan 2025 21:09:21 -0300 Subject: [PATCH 12/18] fix: crash on daily reward (#3215) --- src/creatures/combat/condition.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp index e5219126025..adbb31606e9 100644 --- a/src/creatures/combat/condition.cpp +++ b/src/creatures/combat/condition.cpp @@ -1273,7 +1273,10 @@ bool ConditionRegeneration::executeCondition(const std::shared_ptr &cr const auto &player = creature->getPlayer(); int32_t dailyStreak = 0; if (player) { - dailyStreak = static_cast(player->kv()->scoped("daily-reward")->get("streak")->getNumber()); + auto optStreak = player->kv()->scoped("daily-reward")->get("streak"); + if (optStreak) { + dailyStreak = static_cast(optStreak->getNumber()); + } } if (creature->getZoneType() != ZONE_PROTECTION || dailyStreak >= DAILY_REWARD_HP_REGENERATION) { if (internalHealthTicks >= getHealthTicks(creature)) { From cc3a2465b2258c9b2493b33d5c4ee05869c6216d Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sun, 5 Jan 2025 12:51:38 -0300 Subject: [PATCH 13/18] perf: replace SELECT * with specific columns in SQL queries (#3214) This replaces all occurrences of `SELECT *` in SQL queries with explicit column names. The use of `SELECT *` often retrieves unnecessary data, increasing query processing time and bandwidth usage. By specifying the required columns, this change improves performance and reduces resource usage. --- src/account/account_repository_db.cpp | 2 +- .../players/cyclopedia/player_title.cpp | 4 +- src/game/game.cpp | 62 ++++++++++++------ src/io/functions/iologindata_load_player.cpp | 14 ++-- src/io/io_bosstiary.cpp | 64 ++++++++++++------- 5 files changed, 97 insertions(+), 49 deletions(-) diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index b2e8fd80754..b46d1941779 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -76,7 +76,7 @@ bool AccountRepositoryDB::getCharacterByAccountIdAndName(const uint32_t &id, con } bool AccountRepositoryDB::getPassword(const uint32_t &id, std::string &password) { - auto result = g_database().storeQuery(fmt::format("SELECT * FROM `accounts` WHERE `id` = {}", id)); + auto result = g_database().storeQuery(fmt::format("SELECT `password` FROM `accounts` WHERE `id` = {}", id)); if (!result) { g_logger().error("Failed to get account:[{}] password!", id); return false; diff --git a/src/creatures/players/cyclopedia/player_title.cpp b/src/creatures/players/cyclopedia/player_title.cpp index 089d02f281c..c7e1c8f04f6 100644 --- a/src/creatures/players/cyclopedia/player_title.cpp +++ b/src/creatures/players/cyclopedia/player_title.cpp @@ -224,7 +224,9 @@ bool PlayerTitle::checkHighscore(uint8_t skill) const { default: std::string skillName = g_game().getSkillNameById(skill); query = fmt::format( - "SELECT * FROM `players` WHERE `group_id` < {} AND `{}` > 10 ORDER BY `{}` DESC LIMIT 1", + "SELECT `id` FROM `players` " + "WHERE `group_id` < {} AND `{}` > 10 " + "ORDER BY `{}` DESC LIMIT 1", static_cast(GROUP_TYPE_GAMEMASTER), skillName, skillName ); break; diff --git a/src/game/game.cpp b/src/game/game.cpp index 54a666337d3..b3d558a64d1 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -440,7 +440,10 @@ void Game::resetNpcs() const { void Game::loadBoostedCreature() { auto &db = Database::getInstance(); - const auto result = db.storeQuery("SELECT * FROM `boosted_creature`"); + const auto result = db.storeQuery( + "SELECT `date`, `boostname`, `raceid`, `looktype`, `lookfeet`, `looklegs`, `lookhead`, `lookbody`, `lookaddons`, `lookmount` " + "FROM `boosted_creature`" + ); if (!result) { g_logger().warn("[Game::loadBoostedCreature] - " "Failed to detect boosted creature database. (CODE 01)"); @@ -8514,38 +8517,61 @@ void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, } std::string Game::generateHighscoreQueryForEntries(const std::string &categoryName, uint32_t page, uint8_t entriesPerPage, uint32_t vocation) { - std::ostringstream query; - uint32_t startPage = (static_cast(page - 1) * static_cast(entriesPerPage)); + uint32_t startPage = (page - 1) * static_cast(entriesPerPage); uint32_t endPage = startPage + static_cast(entriesPerPage); - query << "SELECT *, @row AS `entries`, " << page << " AS `page` FROM (SELECT *, (@row := @row + 1) AS `rn` FROM (SELECT `id`, `name`, `level`, `vocation`, `" - << categoryName << "` AS `points`, @curRank := IF(@prevRank = `" << categoryName << "`, @curRank, IF(@prevRank := `" << categoryName - << "`, @curRank + 1, @curRank + 1)) AS `rank` FROM `players` `p`, (SELECT @curRank := 0, @prevRank := NULL, @row := 0) `r` WHERE `group_id` < " - << static_cast(GROUP_TYPE_GAMEMASTER) << " ORDER BY `" << categoryName << "` DESC) `t`"; + Database &db = Database::getInstance(); + std::string escapedCategoryName = db.escapeString(categoryName); + + std::string query = fmt::format( + "SELECT `id`, `name`, `level`, `vocation`, `points`, `rank`, `entries`, {} AS `page` FROM (" + "SELECT `id`, `name`, `level`, `vocation`, `{}` AS `points`, " + "@curRank := IF(@prevRank = `{}`, @curRank, IF(@prevRank := `{}`, @curRank + 1, @curRank + 1)) AS `rank`, " + "(@row := @row + 1) AS `entries` FROM (" + "SELECT `id`, `name`, `level`, `vocation`, `{}` FROM `players` `p`, " + "(SELECT @curRank := 0, @prevRank := NULL, @row := 0) `r` " + "WHERE `group_id` < {} ORDER BY `{}` DESC" + ") `t`", + page, escapedCategoryName, escapedCategoryName, escapedCategoryName, escapedCategoryName, static_cast(GROUP_TYPE_GAMEMASTER), escapedCategoryName + ); if (vocation != 0xFFFFFFFF) { - query << generateVocationConditionHighscore(vocation); + query += generateVocationConditionHighscore(vocation); } - query << ") `T` WHERE `rn` > " << startPage << " AND `rn` <= " << endPage; - return query.str(); + query += fmt::format(") `T` WHERE `entries` > {} AND `entries` <= {}", startPage, endPage); + + return query; } std::string Game::generateHighscoreQueryForOurRank(const std::string &categoryName, uint8_t entriesPerPage, uint32_t playerGUID, uint32_t vocation) { - std::ostringstream query; + Database &db = Database::getInstance(); + std::string escapedCategoryName = db.escapeString(categoryName); std::string entriesStr = std::to_string(entriesPerPage); - query << "SELECT *, @row AS `entries`, (@ourRow DIV " << entriesStr << ") + 1 AS `page` FROM (SELECT *, (@row := @row + 1) AS `rn`, @ourRow := IF(`id` = " - << playerGUID << ", @row - 1, @ourRow) AS `rw` FROM (SELECT `id`, `name`, `level`, `vocation`, `" << categoryName << "` AS `points`, @curRank := IF(@prevRank = `" - << categoryName << "`, @curRank, IF(@prevRank := `" << categoryName << "`, @curRank + 1, @curRank + 1)) AS `rank` FROM `players` `p`, (SELECT @curRank := 0, @prevRank := NULL, @row := 0, @ourRow := 0) `r` WHERE `group_id` < " - << static_cast(GROUP_TYPE_GAMEMASTER) << " ORDER BY `" << categoryName << "` DESC) `t`"; + std::string query = fmt::format( + "SELECT `id`, `name`, `level`, `vocation`, `points`, `rank`, @row AS `entries`, " + "(@ourRow DIV {0}) + 1 AS `page` FROM (" + "SELECT `id`, `name`, `level`, `vocation`, `{1}` AS `points`, " + "@curRank := IF(@prevRank = `{1}`, @curRank, IF(@prevRank := `{1}`, @curRank + 1, @curRank + 1)) AS `rank`, " + "(@row := @row + 1) AS `rn`, @ourRow := IF(`id` = {2}, @row - 1, @ourRow) AS `rw` FROM (" + "SELECT `id`, `name`, `level`, `vocation`, `{1}` FROM `players` `p`, " + "(SELECT @curRank := 0, @prevRank := NULL, @row := 0, @ourRow := 0) `r` " + "WHERE `group_id` < {3} ORDER BY `{1}` DESC" + ") `t`", + entriesStr, escapedCategoryName, playerGUID, static_cast(GROUP_TYPE_GAMEMASTER) + ); if (vocation != 0xFFFFFFFF) { - query << generateVocationConditionHighscore(vocation); + query += generateVocationConditionHighscore(vocation); } - query << ") `T` WHERE `rn` > ((@ourRow DIV " << entriesStr << ") * " << entriesStr << ") AND `rn` <= (((@ourRow DIV " << entriesStr << ") * " << entriesStr << ") + " << entriesStr << ")"; - return query.str(); + query += fmt::format( + ") `T` WHERE `rn` > ((@ourRow DIV {0}) * {0}) AND `rn` <= (((@ourRow DIV {0}) * {0}) + {0})", + entriesStr + ); + + return query; } std::string Game::generateVocationConditionHighscore(uint32_t vocation) { diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index e724d0ceda9..21d251c6dbe 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -869,14 +869,18 @@ 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__); + if (!player) { + g_logger().warn("[{}] - Player nullptr", __FUNCTION__); return; } - std::ostringstream query; - query << "SELECT * FROM `forge_history` WHERE `player_id` = " << player->getGUID(); - if ((result = Database::getInstance().storeQuery(query.str()))) { + auto playerGUID = player->getGUID(); + + auto query = fmt::format( + "SELECT id, action_type, description, done_at, is_success FROM forge_history WHERE player_id = {}", + playerGUID + ); + if ((result = Database::getInstance().storeQuery(query))) { do { auto actionEnum = magic_enum::enum_value(result->getNumber("action_type")); ForgeHistory history; diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 57a9a85e294..80634d11b42 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -20,14 +20,15 @@ IOBosstiary &IOBosstiary::getInstance() { } void IOBosstiary::loadBoostedBoss() { - std::string query = R"SQL( - SELECT `date`, `boostname`, `raceid`, `looktypeEx`, `looktype`, - `lookfeet`, `looklegs`, `lookhead`, `lookbody`, - `lookaddons`, `lookmount` - FROM `boosted_boss` - )SQL"; - - DBResult_ptr result = g_database().storeQuery(query); + Database &database = Database::getInstance(); + auto query = fmt::format("SELECT `date`, `boostname`, `raceid` FROM `boosted_boss`"); + DBResult_ptr result = database.storeQuery(query); + if (!result) { + g_logger().error("[{}] Failed to detect boosted boss database. (CODE 01)", __FUNCTION__); + return; + } + + auto date = result->getNumber("date"); auto timeNow = getTimeNow(); auto time = localtime(&timeNow); auto today = time->tm_mday; @@ -73,34 +74,49 @@ void IOBosstiary::loadBoostedBoss() { uint16_t bossId = randomBossId; query = fmt::format( - "UPDATE `boosted_boss` SET `date` = '{}', `boostname` = {}, ", - today, g_database().escapeString(bossName) + "UPDATE `boosted_boss` SET `date` = '{}', `boostname` = {}, `raceid` = '{}', ", + today, database.escapeString(bossName), bossId ); - if (const auto &bossType = getMonsterTypeByBossRaceId(bossId); bossType) { + + if (const auto bossType = getMonsterTypeByBossRaceId(bossId); bossType) { query += fmt::format( "`looktypeEx` = {}, `looktype` = {}, `lookfeet` = {}, `looklegs` = {}, " "`lookhead` = {}, `lookbody` = {}, `lookaddons` = {}, `lookmount` = {}, ", - bossType->info.outfit.lookTypeEx, bossType->info.outfit.lookType, - bossType->info.outfit.lookFeet, bossType->info.outfit.lookLegs, - bossType->info.outfit.lookHead, bossType->info.outfit.lookBody, - bossType->info.outfit.lookAddons, bossType->info.outfit.lookMount + static_cast(bossType->info.outfit.lookTypeEx), + static_cast(bossType->info.outfit.lookType), + static_cast(bossType->info.outfit.lookFeet), + static_cast(bossType->info.outfit.lookLegs), + static_cast(bossType->info.outfit.lookHead), + static_cast(bossType->info.outfit.lookBody), + static_cast(bossType->info.outfit.lookAddons), + static_cast(bossType->info.outfit.lookMount) ); } - query += fmt::format("`raceid` = {}", bossId); - if (!g_database().executeQuery(query)) { - g_logger().error("[{}] Failed to update boosted boss in g_database(). (CODE 03)", __FUNCTION__); + query += fmt::format("`raceid` = '{}'", bossId); + + if (!database.executeQuery(query)) { + g_logger().error("[{}] Failed to detect boosted boss database. (CODE 03)", __FUNCTION__); return; } - query = fmt::format("UPDATE `player_bosstiary` SET `bossIdSlotOne` = 0 WHERE `bossIdSlotOne` = {}", bossId); - if (!g_database().executeQuery(query)) { - g_logger().error("[{}] Failed to reset players' selected boss slot 1. (CODE 03)", __FUNCTION__); + query = fmt::format( + "UPDATE `player_bosstiary` SET `bossIdSlotOne` = 0 WHERE `bossIdSlotOne` = {}", + bossId + ); + + if (!database.executeQuery(query)) { + g_logger().error("[{}] Failed to reset players selected boss slot 1. (CODE 03)", __FUNCTION__); } - query = fmt::format("UPDATE `player_bosstiary` SET `bossIdSlotTwo` = 0 WHERE `bossIdSlotTwo` = {}", bossId); - if (!g_database().executeQuery(query)) { - g_logger().error("[{}] Failed to reset players' selected boss slot 2. (CODE 03)", __FUNCTION__); + query = fmt::format( + "UPDATE `player_bosstiary` SET `bossIdSlotTwo` = 0 WHERE `bossIdSlotTwo` = {}", + bossId + ); + + if (!database.executeQuery(query)) { + g_logger().error("[{}] Failed to reset players selected boss slot 2. (CODE 03)", __FUNCTION__); + return; } setBossBoostedName(bossName); From 0e188a995639d234a5fc2a11773f80eea88742ee Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sun, 5 Jan 2025 15:08:09 -0300 Subject: [PATCH 14/18] perf: fixes exhaustion to 'playerEquipItem' (#3165) --- src/game/game.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/game/game.cpp b/src/game/game.cpp index b3d558a64d1..e692a3ee3ff 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -3383,6 +3383,21 @@ void Game::playerEquipItem(uint32_t playerId, uint16_t itemId, bool hasTier /* = return; } + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime() - OTSYS_TIME(); + if (delay > 0) { + const auto &task = createPlayerTask( + delay, + [this, playerId, itemId, hasTier, tier] { + playerEquipItem(playerId, itemId, hasTier, tier); + }, + __FUNCTION__ + ); + player->setNextActionTask(task); + } + return; + } + if (player->hasCondition(CONDITION_FEARED)) { /* * When player is feared the player can´t equip any items. @@ -3480,6 +3495,8 @@ void Game::playerEquipItem(uint32_t playerId, uint16_t itemId, bool hasTier /* = if (ret != RETURNVALUE_NOERROR) { player->sendCancelMessage(ret); } + + player->setNextAction(OTSYS_TIME() + g_configManager().getNumber(ACTIONS_DELAY_INTERVAL)); } void Game::playerMove(uint32_t playerId, Direction direction) { From 8aac2a02975c97e8c94738f674fa963534f392d5 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sun, 5 Jan 2025 15:15:54 -0300 Subject: [PATCH 15/18] fix: shadowing "date", move to the correct place (#3219) --- src/io/io_bosstiary.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 80634d11b42..351f8017718 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -28,17 +28,16 @@ void IOBosstiary::loadBoostedBoss() { return; } - auto date = result->getNumber("date"); - auto timeNow = getTimeNow(); - auto time = localtime(&timeNow); - auto today = time->tm_mday; - const auto &bossMap = getBosstiaryMap(); if (bossMap.size() <= 1) { g_logger().error("[{}] It is not possible to create a boosted boss with only one registered boss. (CODE 02)", __FUNCTION__); return; } + auto timeNow = getTimeNow(); + auto time = localtime(&timeNow); + auto today = time->tm_mday; + if (!result) { g_logger().warn("[{}] No boosted boss found in g_database(). A new one will be selected.", __FUNCTION__); } else { From f2533739c1f089092283080371f8b9ea38474e67 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 7 Jan 2025 13:40:56 -0300 Subject: [PATCH 16/18] fix: ensure isPodium check works correctly (#3221) --- src/lua/functions/items/item_type_functions.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lua/functions/items/item_type_functions.cpp b/src/lua/functions/items/item_type_functions.cpp index 9c12f80e586..a1751ed4f15 100644 --- a/src/lua/functions/items/item_type_functions.cpp +++ b/src/lua/functions/items/item_type_functions.cpp @@ -34,6 +34,7 @@ void ItemTypeFunctions::init(lua_State* L) { Lua::registerMethod(L, "ItemType", "isPickupable", ItemTypeFunctions::luaItemTypeIsPickupable); Lua::registerMethod(L, "ItemType", "isKey", ItemTypeFunctions::luaItemTypeIsKey); Lua::registerMethod(L, "ItemType", "isQuiver", ItemTypeFunctions::luaItemTypeIsQuiver); + Lua::registerMethod(L, "ItemType", "isPodium", ItemTypeFunctions::luaItemTypeIsPodium); Lua::registerMethod(L, "ItemType", "getType", ItemTypeFunctions::luaItemTypeGetType); Lua::registerMethod(L, "ItemType", "getId", ItemTypeFunctions::luaItemTypeGetId); From 015169e07367880317f67000ec920cc238315d3f Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Wed, 8 Jan 2025 08:28:20 -0300 Subject: [PATCH 17/18] fix: highscore sql query (#3222) --- src/game/game.cpp | 80 +++++++++++++++++++++++++---------------------- src/game/game.hpp | 9 ++++-- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index e692a3ee3ff..5ce501c8288 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8533,60 +8533,64 @@ void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, } } -std::string Game::generateHighscoreQueryForEntries(const std::string &categoryName, uint32_t page, uint8_t entriesPerPage, uint32_t vocation) { +std::string Game::generateHighscoreQuery( + const std::string &categoryName, + uint32_t page, + uint8_t entriesPerPage, + uint32_t vocation, + uint32_t playerGUID /*= 0*/ +) { uint32_t startPage = (page - 1) * static_cast(entriesPerPage); uint32_t endPage = startPage + static_cast(entriesPerPage); + std::string entriesStr = std::to_string(entriesPerPage); - Database &db = Database::getInstance(); - std::string escapedCategoryName = db.escapeString(categoryName); + if (categoryName.empty()) { + g_logger().error("Category name cannot be empty."); + return ""; + } std::string query = fmt::format( - "SELECT `id`, `name`, `level`, `vocation`, `points`, `rank`, `entries`, {} AS `page` FROM (" - "SELECT `id`, `name`, `level`, `vocation`, `{}` AS `points`, " - "@curRank := IF(@prevRank = `{}`, @curRank, IF(@prevRank := `{}`, @curRank + 1, @curRank + 1)) AS `rank`, " - "(@row := @row + 1) AS `entries` FROM (" - "SELECT `id`, `name`, `level`, `vocation`, `{}` FROM `players` `p`, " - "(SELECT @curRank := 0, @prevRank := NULL, @row := 0) `r` " - "WHERE `group_id` < {} ORDER BY `{}` DESC" - ") `t`", - page, escapedCategoryName, escapedCategoryName, escapedCategoryName, escapedCategoryName, static_cast(GROUP_TYPE_GAMEMASTER), escapedCategoryName + "SELECT `id`, `name`, `level`, `vocation`, `points`, `rank`, `rn` AS `entries`, " ); - if (vocation != 0xFFFFFFFF) { - query += generateVocationConditionHighscore(vocation); + if (playerGUID != 0) { + query += fmt::format("(@ourRow DIV {0}) + 1 AS `page` FROM (", entriesStr); + } else { + query += fmt::format("{} AS `page` FROM (", page); } - query += fmt::format(") `T` WHERE `entries` > {} AND `entries` <= {}", startPage, endPage); - - return query; -} + query += fmt::format( + "SELECT `id`, `name`, `level`, `vocation`, `{}` AS `points`, " + "@curRank := IF(@prevRank = `{}`, @curRank, IF(@prevRank := `{}`, @curRank + 1, @curRank + 1)) AS `rank`, " + "(@row := @row + 1) AS `rn`", + categoryName, categoryName, categoryName + ); -std::string Game::generateHighscoreQueryForOurRank(const std::string &categoryName, uint8_t entriesPerPage, uint32_t playerGUID, uint32_t vocation) { - Database &db = Database::getInstance(); - std::string escapedCategoryName = db.escapeString(categoryName); - std::string entriesStr = std::to_string(entriesPerPage); + if (playerGUID != 0) { + query += fmt::format(", @ourRow := IF(`id` = {}, @row - 1, @ourRow) AS `rw`", playerGUID); + } - std::string query = fmt::format( - "SELECT `id`, `name`, `level`, `vocation`, `points`, `rank`, @row AS `entries`, " - "(@ourRow DIV {0}) + 1 AS `page` FROM (" - "SELECT `id`, `name`, `level`, `vocation`, `{1}` AS `points`, " - "@curRank := IF(@prevRank = `{1}`, @curRank, IF(@prevRank := `{1}`, @curRank + 1, @curRank + 1)) AS `rank`, " - "(@row := @row + 1) AS `rn`, @ourRow := IF(`id` = {2}, @row - 1, @ourRow) AS `rw` FROM (" - "SELECT `id`, `name`, `level`, `vocation`, `{1}` FROM `players` `p`, " + query += fmt::format( + " FROM (SELECT `id`, `name`, `level`, `vocation`, `{}` FROM `players` `p`, " "(SELECT @curRank := 0, @prevRank := NULL, @row := 0, @ourRow := 0) `r` " - "WHERE `group_id` < {3} ORDER BY `{1}` DESC" - ") `t`", - entriesStr, escapedCategoryName, playerGUID, static_cast(GROUP_TYPE_GAMEMASTER) + "WHERE `group_id` < {} ORDER BY `{}` DESC) `t`", + categoryName, static_cast(GROUP_TYPE_GAMEMASTER), categoryName ); if (vocation != 0xFFFFFFFF) { query += generateVocationConditionHighscore(vocation); } - query += fmt::format( - ") `T` WHERE `rn` > ((@ourRow DIV {0}) * {0}) AND `rn` <= (((@ourRow DIV {0}) * {0}) + {0})", - entriesStr - ); + query += ") `T` WHERE "; + + if (playerGUID != 0) { + query += fmt::format( + "`rn` > ((@ourRow DIV {0}) * {0}) AND `rn` <= (((@ourRow DIV {0}) * {0}) + {0})", + entriesStr + ); + } else { + query += fmt::format("`rn` > {} AND `rn` <= {}", startPage, endPage); + } return query; } @@ -8676,7 +8680,7 @@ std::string Game::generateHighscoreOrGetCachedQueryForEntries(const std::string } } - std::string newQuery = generateHighscoreQueryForEntries(categoryName, page, entriesPerPage, vocation); + std::string newQuery = generateHighscoreQuery(categoryName, page, entriesPerPage, vocation); cacheQueryHighscore(cacheKey, newQuery, page, entriesPerPage); return newQuery; @@ -8694,7 +8698,7 @@ std::string Game::generateHighscoreOrGetCachedQueryForOurRank(const std::string } } - std::string newQuery = generateHighscoreQueryForOurRank(categoryName, entriesPerPage, playerGUID, vocation); + std::string newQuery = generateHighscoreQuery(categoryName, 0, entriesPerPage, vocation, playerGUID); cacheQueryHighscore(cacheKey, newQuery, entriesPerPage, entriesPerPage); return newQuery; diff --git a/src/game/game.hpp b/src/game/game.hpp index a59c7d13a70..9a4b59f8a55 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -957,8 +957,13 @@ class Game { void processHighscoreResults(const DBResult_ptr &result, uint32_t playerID, uint8_t category, uint32_t vocation, uint8_t entriesPerPage); std::string generateVocationConditionHighscore(uint32_t vocation); - std::string generateHighscoreQueryForEntries(const std::string &categoryName, uint32_t page, uint8_t entriesPerPage, uint32_t vocation); - std::string generateHighscoreQueryForOurRank(const std::string &categoryName, uint8_t entriesPerPage, uint32_t playerGUID, uint32_t vocation); + std::string generateHighscoreQuery( + const std::string &categoryName, + uint32_t page, + uint8_t entriesPerPage, + uint32_t vocation, + uint32_t playerGUID = 0 + ); std::string generateHighscoreOrGetCachedQueryForEntries(const std::string &categoryName, uint32_t page, uint8_t entriesPerPage, uint32_t vocation); std::string generateHighscoreOrGetCachedQueryForOurRank(const std::string &categoryName, uint8_t entriesPerPage, uint32_t playerGUID, uint32_t vocation); From 7c10e50f47bf3c5f05489179047984798211d32d Mon Sep 17 00:00:00 2001 From: "Leilani A." <168607226+kaleohanopahala@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:02:31 -0300 Subject: [PATCH 18/18] feat: add morguthis wall action (#3226) --- .../action_morguthis_wall.lua | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 data-otservbr-global/scripts/quests/the_ancient_tombs/action_morguthis_wall.lua diff --git a/data-otservbr-global/scripts/quests/the_ancient_tombs/action_morguthis_wall.lua b/data-otservbr-global/scripts/quests/the_ancient_tombs/action_morguthis_wall.lua new file mode 100644 index 00000000000..420a2f3fbc2 --- /dev/null +++ b/data-otservbr-global/scripts/quests/the_ancient_tombs/action_morguthis_wall.lua @@ -0,0 +1,39 @@ +local morguthisWall = Action() + +function morguthisWall.onUse(player, item, fromPosition, target, toPosition) + local wallPosition = Position(33211, 32698, 13) + local wallId = 1306 + + local tile = Tile(wallPosition) + if not tile then + return false + end + + local wall = tile:getItemById(wallId) + if wall then + wall:remove() + else + local creatures = tile:getCreatures() + if creatures then + for _, creature in ipairs(creatures) do + local newPosition = Position(wallPosition.x, wallPosition.y + 1, wallPosition.z) + creature:teleportTo(newPosition) + end + end + + local items = tile:getItems() + if items then + for _, tileItem in ipairs(items) do + local newPosition = Position(wallPosition.x, wallPosition.y + 1, wallPosition.z) + tileItem:moveTo(newPosition) + end + end + + Game.createItem(wallId, 1, wallPosition) + end + + return true +end + +morguthisWall:position(Position(33212, 32693, 13)) +morguthisWall:register()