From 74bd12619e3dbaeffbd31be74fc3e41ae6ee8529 Mon Sep 17 00:00:00 2001 From: pudimtibia Date: Tue, 15 Oct 2024 18:41:53 -0300 Subject: [PATCH 1/8] fix: disable ssl/tls verification for newer version of mysql (#2978) The directive included in the connection options ignores the SSL check. --- src/database/database.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/database/database.cpp b/src/database/database.cpp index 9ac892ef2be..4f038605e05 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -42,6 +42,10 @@ bool Database::connect(const std::string* host, const std::string* user, const s bool reconnect = true; mysql_options(handle, MYSQL_OPT_RECONNECT, &reconnect); + // Remove ssl verification + bool ssl_enabled = false; + mysql_options(handle, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &ssl_enabled); + // connects to database if (!mysql_real_connect(handle, host->c_str(), user->c_str(), password->c_str(), database->c_str(), port, sock->c_str(), 0)) { g_logger().error("MySQL Error Message: {}", mysql_error(handle)); From 0c7aafe0123f9f58d9b59c5a5acbabcba26cb52c Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Tue, 15 Oct 2024 18:43:58 -0300 Subject: [PATCH 2/8] perf: optimized account players badge loading (#2977) This change optimizes the player information retrieval process by selecting only the pertinent details of each character instead of loading all player objects for an account. This prevents unnecessary memory allocation and reduces server load, especially in scenarios where accounts have multiple characters. --- .../players/cyclopedia/player_badge.cpp | 36 +++++++++++++++++-- .../players/cyclopedia/player_badge.hpp | 2 ++ src/creatures/players/player.hpp | 3 ++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/creatures/players/cyclopedia/player_badge.cpp b/src/creatures/players/cyclopedia/player_badge.cpp index c19e2443db1..b509ef041e3 100644 --- a/src/creatures/players/cyclopedia/player_badge.cpp +++ b/src/creatures/players/cyclopedia/player_badge.cpp @@ -13,6 +13,8 @@ #include "game/game.hpp" #include "kv/kv.hpp" +#include "enums/account_errors.hpp" + PlayerBadge::PlayerBadge(Player &player) : m_player(player) { } @@ -113,8 +115,38 @@ bool PlayerBadge::loyalty(uint8_t amount) { return m_player.getLoyaltyPoints() >= amount; } +std::vector> PlayerBadge::getPlayersInfoByAccount(std::shared_ptr acc) const { + auto [accountPlayers, error] = acc->getAccountPlayers(); + if (error != enumToValue(AccountErrors_t::Ok) || accountPlayers.empty()) { + return {}; + } + + std::string namesList; + for (const auto &[name, _] : accountPlayers) { + if (!namesList.empty()) { + namesList += ", "; + } + namesList += fmt::format("'{}'", name); + } + + auto query = fmt::format("SELECT name, level, vocation FROM players WHERE name IN ({})", namesList); + std::vector> players; + DBResult_ptr result = g_database().storeQuery(query); + if (result) { + do { + auto player = std::make_shared(nullptr); + player->setName(result->getString("name")); + player->setLevel(result->getNumber("level")); + player->setVocation(result->getNumber("vocation")); + players.push_back(player); + } while (result->next()); + } + + return players; +} + bool PlayerBadge::accountAllLevel(uint8_t amount) { - auto players = g_game().getPlayersByAccount(m_player.getAccount(), true); + auto players = getPlayersInfoByAccount(m_player.getAccount()); uint16_t total = std::accumulate(players.begin(), players.end(), 0, [](uint16_t sum, const std::shared_ptr &player) { return sum + player->getLevel(); }); @@ -126,7 +158,7 @@ bool PlayerBadge::accountAllVocations(uint8_t amount) { auto paladin = false; auto druid = false; auto sorcerer = false; - for (const auto &player : g_game().getPlayersByAccount(m_player.getAccount(), true)) { + for (const auto &player : getPlayersInfoByAccount(m_player.getAccount())) { if (player->getLevel() >= amount) { auto vocationEnum = player->getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { diff --git a/src/creatures/players/cyclopedia/player_badge.hpp b/src/creatures/players/cyclopedia/player_badge.hpp index 7bf28c0c302..25eaea0666f 100644 --- a/src/creatures/players/cyclopedia/player_badge.hpp +++ b/src/creatures/players/cyclopedia/player_badge.hpp @@ -13,6 +13,7 @@ class Player; class KV; +class Account; struct Badge { uint8_t m_id = 0; @@ -52,6 +53,7 @@ class PlayerBadge { // Badge Calculate Functions bool accountAge(uint8_t amount); bool loyalty(uint8_t amount); + std::vector> getPlayersInfoByAccount(std::shared_ptr acc) const; bool accountAllLevel(uint8_t amount); bool accountAllVocations(uint8_t amount); [[nodiscard]] bool tournamentParticipation(uint8_t skill); diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index ff844c97b3b..cd69cec99a0 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -606,6 +606,9 @@ class Player final : public Creature, public Cylinder, public Bankable { uint32_t getLevel() const { return level; } + void setLevel(uint32_t newLevel) { + level = newLevel; + } uint8_t getLevelPercent() const { return levelPercent; } From 68e9c3b5995e76280bd7484dd025f0371c30dfd1 Mon Sep 17 00:00:00 2001 From: Leonardo <130802152+LeoPetryx@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:43:06 -0300 Subject: [PATCH 3/8] fix: bug when player dies with skillLost false (#2979) Changed the location of the function to execute in all scenarios. --- src/creatures/players/player.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 4470d7431ad..27e113b852b 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -2903,6 +2903,7 @@ void Player::death(std::shared_ptr lastHitCreature) { ++it; } } + despawn(); } else { setSkillLoss(true); @@ -2927,8 +2928,6 @@ void Player::death(std::shared_ptr lastHitCreature) { onIdleStatus(); sendStats(); } - - despawn(); } bool Player::spawn() { From e70c8c5bc96bd32296d3078505c643c30408e484 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 17 Oct 2024 16:47:45 -0300 Subject: [PATCH 4/8] refactor: modularize look event callback and improve code readability (#2858) This commit refactors the code related to the onLook event by modularizing the logic into separate functions for handling item and creature descriptions. Additionally, admin-specific details have been moved to a dedicated function. These changes improve readability, reduce code repetition, and make future maintenance easier. Resolves #2856 --- .../scripts/eventcallbacks/player/on_look.lua | 188 +++++++++++------- 1 file changed, 113 insertions(+), 75 deletions(-) diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 851cd9b707f..68824cb9b44 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -1,91 +1,129 @@ -local callback = EventCallback("PlayerOnLookBaseEvent") +local specialItemRanges = { + { rangeStart = ITEM_HEALTH_CASK_START, rangeEnd = ITEM_HEALTH_CASK_END }, + { rangeStart = ITEM_MANA_CASK_START, rangeEnd = ITEM_MANA_CASK_END }, + { rangeStart = ITEM_SPIRIT_CASK_START, rangeEnd = ITEM_SPIRIT_CASK_END }, + { rangeStart = ITEM_KEG_START, rangeEnd = ITEM_KEG_END }, +} -function callback.playerOnLook(player, thing, position, distance) - local description = "You see " - if thing:isItem() then - if thing.actionid == 5640 then - description = description .. "a honeyflower patch." - elseif thing.actionid == 5641 then - description = description .. "a banana palm." - elseif thing.itemid >= ITEM_HEALTH_CASK_START and thing.itemid <= ITEM_HEALTH_CASK_END or thing.itemid >= ITEM_MANA_CASK_START and thing.itemid <= ITEM_MANA_CASK_END or thing.itemid >= ITEM_SPIRIT_CASK_START and thing.itemid <= ITEM_SPIRIT_CASK_END or thing.itemid >= ITEM_KEG_START and thing.itemid <= ITEM_KEG_END then - description = description .. thing:getDescription(distance) - local charges = thing:getCharges() - if charges > 0 then - description = string.format("%s\nIt has %d refillings left.", description, charges) - end - else - description = description .. thing:getDescription(distance) +local function isSpecialItem(itemId) + for _, range in ipairs(specialItemRanges) do + if itemId >= range.rangeStart and itemId <= range.rangeEnd then + return true end - local ownerName = thing:getOwnerName() - if ownerName then - description = string.format("%s\nIt belongs to %s.", description, ownerName) + end + return false +end + +local function getPositionDescription(position) + if position.x == 65535 then + return "Position: In your inventory." + else + return string.format("Position: (%d, %d, %d)", position.x, position.y, position.z) + end +end + +local function handleItemDescription(inspectedThing, lookDistance) + local descriptionText = inspectedThing:getDescription(lookDistance) + + if isSpecialItem(inspectedThing.itemid) then + local itemCharges = inspectedThing:getCharges() + if itemCharges > 0 then + return string.format("You see %s\nIt has %d refillings left.", descriptionText, itemCharges) end else - description = description .. thing:getDescription(distance) - if thing:isMonster() then - local master = thing:getMaster() - if master and table.contains({ "sorcerer familiar", "knight familiar", "druid familiar", "paladin familiar" }, thing:getName():lower()) then - local familiarSummonTime = master:kv():get("familiar-summon-time") or 0 - description = string.format("%s (Master: %s). \z It will disappear in %s", description, master:getName(), getTimeInWords(familiarSummonTime - os.time())) - end + return "You see " .. descriptionText + end + + return descriptionText +end + +local function handleCreatureDescription(inspectedThing, lookDistance) + local descriptionText = inspectedThing:getDescription(lookDistance) + + if inspectedThing:isMonster() then + local monsterMaster = inspectedThing:getMaster() + if monsterMaster and table.contains({ "sorcerer familiar", "knight familiar", "druid familiar", "paladin familiar" }, inspectedThing:getName():lower()) then + local summonTimeRemaining = monsterMaster:kv():get("familiar-summon-time") or 0 + descriptionText = string.format("%s (Master: %s). It will disappear in %s", descriptionText, monsterMaster:getName(), getTimeInWords(summonTimeRemaining - os.time())) end end - if player:getGroup():getAccess() then - if thing:isItem() then - description = string.format("%s\nClient ID: %d", description, thing:getId()) - - local actionId = thing:getActionId() - if actionId ~= 0 then - description = string.format("%s, Action ID: %d", description, actionId) - end - - local uniqueId = thing:getAttribute(ITEM_ATTRIBUTE_UNIQUEID) - if uniqueId > 0 and uniqueId < 65536 then - description = string.format("%s, Unique ID: %d", description, uniqueId) - end - - local itemType = thing:getType() - - local transformEquipId = itemType:getTransformEquipId() - local transformDeEquipId = itemType:getTransformDeEquipId() - if transformEquipId ~= 0 then - description = string.format("%s\nTransforms to: %d (onEquip)", description, transformEquipId) - elseif transformDeEquipId ~= 0 then - description = string.format("%s\nTransforms to: %d (onDeEquip)", description, transformDeEquipId) - end - - local decayId = itemType:getDecayId() - if decayId ~= -1 then - description = string.format("%s\nDecays to: %d", description, decayId) - end - elseif thing:isCreature() then - local str, id = "%s\n%s\nHealth: %d / %d" - if thing:isPlayer() and thing:getMaxMana() > 0 then - id = string.format("Player ID: %i", thing:getGuid()) - str = string.format("%s, Mana: %d / %d", str, thing:getMana(), thing:getMaxMana()) - elseif thing:isMonster() then - id = string.format("Monster ID: %i", thing:getId()) - elseif thing:isNpc() then - id = string.format("NPC ID: %i", thing:getId()) - end - description = string.format(str, description, id, thing:getHealth(), thing:getMaxHealth()) + return "You see " .. descriptionText +end + +local function appendAdminDetails(descriptionText, inspectedThing, inspectedPosition) + if inspectedThing:isItem() then + descriptionText = string.format("%s\nClient ID: %d", descriptionText, inspectedThing:getId()) + + local itemActionId = inspectedThing:getActionId() + if itemActionId ~= 0 then + descriptionText = string.format("%s, Action ID: %d", descriptionText, itemActionId) end - description = string.format("%s\nPosition: (%d, %d, %d)", description, position.x, position.y, position.z) + local itemUniqueId = inspectedThing:getUniqueId() + if itemUniqueId > 0 and itemUniqueId < 65536 then + descriptionText = string.format("%s, Unique ID: %d", descriptionText, itemUniqueId) + end - if thing:isCreature() then - local speedBase = thing:getBaseSpeed() - local speed = thing:getSpeed() - description = string.format("%s\nSpeedBase: %d", description, speedBase) - description = string.format("%s\nSpeed: %d", description, speed) + local itemType = inspectedThing:getType() + local transformOnEquipId = itemType:getTransformEquipId() + local transformOnDeEquipId = itemType:getTransformDeEquipId() - if thing:isPlayer() then - description = string.format("%s\nIP: %s", description, Game.convertIpToString(thing:getIp())) - end + if transformOnEquipId ~= 0 then + descriptionText = string.format("%s\nTransforms to: %d (onEquip)", descriptionText, transformOnEquipId) + elseif transformOnDeEquipId ~= 0 then + descriptionText = string.format("%s\nTransforms to: %d (onDeEquip)", descriptionText, transformOnDeEquipId) + end + + local itemDecayId = itemType:getDecayId() + if itemDecayId ~= -1 then + descriptionText = string.format("%s\nDecays to: %d", descriptionText, itemDecayId) + end + elseif inspectedThing:isCreature() then + local healthDescription, creatureId = "%s\n%s\nHealth: %d / %d" + if inspectedThing:isPlayer() and inspectedThing:getMaxMana() > 0 then + creatureId = string.format("Player ID: %i", inspectedThing:getGuid()) + healthDescription = string.format("%s, Mana: %d / %d", healthDescription, inspectedThing:getMana(), inspectedThing:getMaxMana()) + elseif inspectedThing:isMonster() then + creatureId = string.format("Monster ID: %i", inspectedThing:getId()) + elseif inspectedThing:isNpc() then + creatureId = string.format("NPC ID: %i", inspectedThing:getId()) + end + + descriptionText = string.format(healthDescription, descriptionText, creatureId, inspectedThing:getHealth(), inspectedThing:getMaxHealth()) + end + + descriptionText = string.format("%s\n%s", descriptionText, getPositionDescription(inspectedPosition)) + + if inspectedThing:isCreature() then + local creatureBaseSpeed = inspectedThing:getBaseSpeed() + local creatureCurrentSpeed = inspectedThing:getSpeed() + descriptionText = string.format("%s\nSpeed Base: %d\nSpeed: %d", descriptionText, creatureBaseSpeed, creatureCurrentSpeed) + + if inspectedThing:isPlayer() then + descriptionText = string.format("%s\nIP: %s", descriptionText, Game.convertIpToString(inspectedThing:getIp())) end end - player:sendTextMessage(MESSAGE_LOOK, description) + + return descriptionText +end + +local callback = EventCallback("PlayerOnLookBaseEvent") + +function callback.playerOnLook(player, inspectedThing, inspectedPosition, lookDistance) + local descriptionText + + if inspectedThing:isItem() then + descriptionText = handleItemDescription(inspectedThing, lookDistance) + elseif inspectedThing:isCreature() then + descriptionText = handleCreatureDescription(inspectedThing, lookDistance) + end + + if player:getGroup():getAccess() then + descriptionText = appendAdminDetails(descriptionText, inspectedThing, inspectedPosition) + end + + player:sendTextMessage(MESSAGE_LOOK, descriptionText) end callback:register() From f05fb5dda451296dd141d33a983077556a9142e1 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 17 Oct 2024 16:49:19 -0300 Subject: [PATCH 5/8] improve: some fixes and adjustments (#2980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: • In the solution build • In the debug build • Check for race id 0 in prey and taskhunting • Fixes in pop_front of some queues • Fix in Argon2::parseBitShift for debug build • Fix in NetworkMessage functions for debug build • Added try/catch in IOLoginData::savePlayer to avoid unhandled exceptions • New caseInsensitiveCompare function for use in some ongoing pull requests --- src/io/functions/iologindata_load_player.cpp | 16 +++-- src/io/functions/iologindata_save_player.cpp | 4 +- src/io/iologindata.cpp | 18 ++++-- src/security/argon.cpp | 2 +- src/server/network/message/networkmessage.cpp | 17 ++---- src/server/network/protocol/protocolgame.cpp | 11 ++-- src/server/network/webhook/webhook.cpp | 7 ++- src/utils/tools.cpp | 15 +++++ src/utils/tools.hpp | 2 + vcproj/canary.vcxproj | 4 +- vcproj/settings.props | 59 +++++++------------ 11 files changed, 85 insertions(+), 70 deletions(-) diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 80881db361c..cb24c092ebb 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -734,6 +734,10 @@ void IOLoginDataLoad::loadPlayerPreyClass(std::shared_ptr player, DBResu query << "SELECT * FROM `player_prey` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { + auto selectedRaceId = result->getNumber("raceid"); + if (selectedRaceId == 0) { + continue; + } auto slot = std::make_unique(static_cast(result->getNumber("slot"))); auto state = static_cast(result->getNumber("state")); if (slot->id == PreySlot_Two && state == PreyDataState_Locked) { @@ -745,7 +749,7 @@ void IOLoginDataLoad::loadPlayerPreyClass(std::shared_ptr player, DBResu } else { slot->state = state; } - slot->selectedRaceId = result->getNumber("raceid"); + slot->selectedRaceId = selectedRaceId; slot->option = static_cast(result->getNumber("option")); slot->bonus = static_cast(result->getNumber("bonus_type")); slot->bonusRarity = static_cast(result->getNumber("bonus_rarity")); @@ -781,6 +785,10 @@ void IOLoginDataLoad::loadPlayerTaskHuntingClass(std::shared_ptr player, query << "SELECT * FROM `player_taskhunt` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { + auto selectedRaceId = result->getNumber("raceid"); + if (selectedRaceId == 0) { + continue; + } auto slot = std::make_unique(static_cast(result->getNumber("slot"))); auto state = static_cast(result->getNumber("state")); if (slot->id == PreySlot_Two && state == PreyTaskDataState_Locked) { @@ -792,7 +800,7 @@ void IOLoginDataLoad::loadPlayerTaskHuntingClass(std::shared_ptr player, } else { slot->state = state; } - slot->selectedRaceId = result->getNumber("raceid"); + slot->selectedRaceId = selectedRaceId; slot->upgrade = result->getNumber("upgrade"); slot->rarity = static_cast(result->getNumber("rarity")); slot->currentKills = result->getNumber("kills"); @@ -827,7 +835,7 @@ void IOLoginDataLoad::loadPlayerForgeHistory(std::shared_ptr player, DBR std::ostringstream query; query << "SELECT * FROM `forge_history` WHERE `player_id` = " << player->getGUID(); - if (result = Database::getInstance().storeQuery(query.str())) { + if ((result = Database::getInstance().storeQuery(query.str()))) { do { auto actionEnum = magic_enum::enum_value(result->getNumber("action_type")); ForgeHistory history; @@ -853,7 +861,7 @@ void IOLoginDataLoad::loadPlayerBosstiary(std::shared_ptr player, DBResu std::ostringstream query; query << "SELECT * FROM `player_bosstiary` WHERE `player_id` = " << player->getGUID(); - if (result = Database::getInstance().storeQuery(query.str())) { + if ((result = Database::getInstance().storeQuery(query.str()))) { do { player->setSlotBossId(1, result->getNumber("bossIdSlotOne")); player->setSlotBossId(2, result->getNumber("bossIdSlotTwo")); diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index a5caadd709c..f320a67ca22 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -82,7 +82,6 @@ bool IOLoginDataSave::saveItems(std::shared_ptr player, const ItemBlockL const ContainerBlock &cb = queue.front(); std::shared_ptr container = cb.first; int32_t parentId = cb.second; - queue.pop_front(); if (!container) { continue; // Check for null container @@ -137,6 +136,9 @@ bool IOLoginDataSave::saveItems(std::shared_ptr player, const ItemBlockL return false; } } + + // Removes the object after processing everything, avoiding memory usage after freeing + queue.pop_front(); } // Execute query diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index d834f9d0984..17816f969de 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -202,15 +202,21 @@ bool IOLoginData::loadPlayer(std::shared_ptr player, DBResult_ptr result } bool IOLoginData::savePlayer(std::shared_ptr player) { - bool success = DBTransaction::executeWithinTransaction([player]() { - return savePlayerGuard(player); - }); + try { + bool success = DBTransaction::executeWithinTransaction([player]() { + return savePlayerGuard(player); + }); + + if (!success) { + g_logger().error("[{}] Error occurred saving player", __FUNCTION__); + } - if (!success) { - g_logger().error("[{}] Error occurred saving player", __FUNCTION__); + return success; + } catch (const DatabaseException &e) { + g_logger().error("[{}] Exception occurred: {}", __FUNCTION__, e.what()); } - return success; + return false; } bool IOLoginData::savePlayerGuard(std::shared_ptr player) { diff --git a/src/security/argon.cpp b/src/security/argon.cpp index 1bed157e418..52fa4b0c94b 100644 --- a/src/security/argon.cpp +++ b/src/security/argon.cpp @@ -25,7 +25,7 @@ void Argon2::updateConstants() { } uint32_t Argon2::parseBitShift(const std::string &bitShiftStr) const { - static const std::regex pattern(R"(^\s*(\d+)\s*<<\s*(\d+)\s*$)"); + static const std::regex pattern(R"(^\s*(\d+)\s*<<\s*(\d+)\s*$)", std::regex_constants::ECMAScript | std::regex_constants::icase); std::smatch match; if (!std::regex_match(bitShiftStr, match, pattern)) { diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index edce96111cb..5b7c8a143b9 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -160,8 +160,7 @@ void NetworkMessage::addString(const std::string &value, const std::source_locat auto len = static_cast(stringLen); add(len); // Using to copy the string into the buffer - auto it = std::ranges::copy(value, buffer.begin() + info.position); - g_logger().trace("First value copied from sourceSpan: {}, second value copied from sourceSpan: {}", *it.in, *it.out); + std::ranges::copy(value, buffer.begin() + info.position); info.position += stringLen; info.length += stringLen; } @@ -211,8 +210,7 @@ void NetworkMessage::addBytes(const char* bytes, size_t size) { return; } - auto it = std::ranges::copy(bytes, bytes + size, buffer.begin() + info.position); - g_logger().trace("First value copied from sourceSpan: {}, second value copied from sourceSpan: {}", *it.in, *it.out); + std::ranges::copy(std::span(bytes, size), buffer.begin() + info.position); info.position += size; info.length += size; } @@ -293,13 +291,10 @@ void NetworkMessage::append(const NetworkMessage &other) { return; } - // Create a span for the source data (from the other message) - std::span sourceSpan(other.getBuffer() + otherStartPos, otherLength); - // Create a span for the destination in the current buffer - std::span destSpan(buffer.data() + info.position, otherLength); - // Copy the data from the source span to the destination span - auto it = std::ranges::copy(sourceSpan, destSpan.begin()); - g_logger().trace("First value copied from sourceSpan: {}, second value copied from sourceSpan: {}", *it.in, *it.out); + std::ranges::copy( + std::span(other.getBuffer() + otherStartPos, otherLength), + buffer.data() + info.position + ); // Update the buffer information info.length += otherLength; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 7ba07d13e4d..e924b489c25 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -6091,16 +6091,17 @@ void ProtocolGame::sendTradeItemRequest(const std::string &traderName, std::shar std::list> listContainer { tradeContainer }; std::list> itemList { tradeContainer }; while (!listContainer.empty()) { - std::shared_ptr container = listContainer.front(); - listContainer.pop_front(); - - for (const std::shared_ptr &containerItem : container->getItemList()) { - std::shared_ptr tmpContainer = containerItem->getContainer(); + const auto &container = listContainer.front(); + for (const auto &containerItem : container->getItemList()) { + const auto &tmpContainer = containerItem->getContainer(); if (tmpContainer) { listContainer.push_back(tmpContainer); } itemList.push_back(containerItem); } + + // Removes the object after processing everything, avoiding memory usage after freeing + listContainer.pop_front(); } msg.addByte(itemList.size()); diff --git a/src/server/network/webhook/webhook.cpp b/src/server/network/webhook/webhook.cpp index bd0ab9a0c4c..6d28e263e54 100644 --- a/src/server/network/webhook/webhook.cpp +++ b/src/server/network/webhook/webhook.cpp @@ -147,7 +147,7 @@ void Webhook::sendWebhook() { return; } - auto task = webhooks.front(); + const auto &task = webhooks.front(); std::string response_body; auto response_code = sendRequest(task->url.c_str(), task->payload.c_str(), &response_body); @@ -162,8 +162,6 @@ void Webhook::sendWebhook() { return; } - webhooks.pop_front(); - if (response_code >= 300) { g_logger().error( "Failed to send webhook message, error code: {} response body: {} request body: {}", @@ -176,4 +174,7 @@ void Webhook::sendWebhook() { } g_logger().debug("Webhook successfully sent to {}", task->url); + + // Removes the object after processing everything, avoiding memory usage after freeing + webhooks.pop_front(); } diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index 8df1fde1065..df340a67f39 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1952,3 +1952,18 @@ uint8_t convertWheelGemAffinityToDomain(uint8_t affinity) { return 0; } } + +bool caseInsensitiveCompare(std::string_view str1, std::string_view str2, size_t length /*= std::string_view::npos*/) { + if (length == std::string_view::npos) { + if (str1.size() != str2.size()) { + return false; + } + length = str1.size(); + } else { + length = std::min({ length, str1.size(), str2.size() }); + } + + return std::equal(str1.begin(), str1.begin() + length, str2.begin(), [](char c1, char c2) { + return std::tolower(static_cast(c1)) == std::tolower(static_cast(c2)); + }); +} diff --git a/src/utils/tools.hpp b/src/utils/tools.hpp index a4426a066a8..7e04132b36e 100644 --- a/src/utils/tools.hpp +++ b/src/utils/tools.hpp @@ -220,3 +220,5 @@ template (value); } + +bool caseInsensitiveCompare(std::string_view str1, std::string_view str2, size_t length = std::string_view::npos); diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index d2d8e1646c6..135389e491d 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -516,7 +516,7 @@ true true Default - /Zc:__cplusplus /fsanitize=address %(AdditionalOptions) + /Zc:__cplusplus /fsanitize=address /utf-8 %(AdditionalOptions) _DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION;%(PreprocessorDefinitions) Use @@ -552,7 +552,7 @@ true true true - /Zc:__cplusplus %(AdditionalOptions) + /Zc:__cplusplus /utf-8 %(AdditionalOptions) _DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION;NDEBUG;%(PreprocessorDefinitions) Use diff --git a/vcproj/settings.props b/vcproj/settings.props index 6f98969614d..8c6bc887fbc 100644 --- a/vcproj/settings.props +++ b/vcproj/settings.props @@ -8,59 +8,37 @@ _WIN32_WINNT=0x0501; BUILD_TYPE="RelWithDebInfo"; - + comctl32.lib; User32.lib; WS2_32.lib; pugixml.lib; - libprotobuf.lib; lua51.lib; mpir.lib; libmariadb.lib; + abseil_dll.lib; + argon2.lib; + + + $(CANARY_COMMON_LIBDEPS); + libprotobuf.lib; zlib.lib; libcurl.lib; fmt.lib; spdlog.lib; - abseil_dll.lib; - argon2.lib; - opentelemetry_common.lib; - opentelemetry_exporter_in_memory.lib; - opentelemetry_exporter_ostream_logs.lib; - opentelemetry_exporter_ostream_metrics.lib; - opentelemetry_exporter_ostream_span.lib; - opentelemetry_exporter_otlp_http.lib; - opentelemetry_exporter_otlp_http_client.lib; - opentelemetry_exporter_otlp_http_log.lib; - opentelemetry_exporter_otlp_http_metric.lib; - opentelemetry_exporter_prometheus.lib; - opentelemetry_http_client_curl.lib; - opentelemetry_logs.lib; - opentelemetry_metrics.lib; - opentelemetry_otlp_recordable.lib; - opentelemetry_proto.lib; - opentelemetry_resources.lib; - opentelemetry_trace.lib; - opentelemetry_version.lib; - prometheus-cpp-core.lib; - prometheus-cpp-pull.lib; - civetweb.lib; - civetweb-cpp.lib - comctl32.lib; - User32.lib; - WS2_32.lib; - pugixml.lib; + $(CANARY_COMMON_LIBDEPS); libprotobufd.lib; - lua51.lib; - mpir.lib; - libmariadb.lib; zlibd.lib; libcurl-d.lib; fmtd.lib; spdlogd.lib; - abseil_dll.lib; - argon2.lib; + + + + + opentelemetry_common.lib; opentelemetry_exporter_in_memory.lib; opentelemetry_exporter_ostream_logs.lib; @@ -79,13 +57,19 @@ opentelemetry_resources.lib; opentelemetry_trace.lib; opentelemetry_version.lib; + prometheus-cpp-core.lib; + prometheus-cpp-pull; civetweb.lib; - civetweb-cpp.lib - + civetweb-cpp.lib; + + $(CANARY_LIBDEPS);$(OPENTELEMETRY_LIBS) + $(CANARY_LIBDEPS_D);$(OPENTELEMETRY_LIBS) + true + Level3 @@ -101,6 +85,7 @@ mainCRTStartup + $(PREPROCESSOR_DEFS) From 665e90c169ca292decdc33611e304e946d6ab432 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 17 Oct 2024 21:56:41 -0300 Subject: [PATCH 6/8] improve: change filestream/fileloader to std::ranges::copy (#2984) Revert accidental prey loading change from: #2980 Refactored FileStream and FileLoader to utilize `std::ranges::copy` and other modern C++ features, replacing `memcpy` and `reinterpret_cast` for better readability and maintainability. --- src/io/fileloader.hpp | 39 ++++++++++++++------ src/io/filestream.cpp | 26 ++++++++----- src/io/functions/iologindata_load_player.cpp | 12 +----- src/io/functions/iologindata_save_player.cpp | 4 +- src/io/ioprey.cpp | 5 ++- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/io/fileloader.hpp b/src/io/fileloader.hpp index 2069487202f..863837464ae 100644 --- a/src/io/fileloader.hpp +++ b/src/io/fileloader.hpp @@ -22,9 +22,9 @@ namespace OTB { Node &operator=(const Node &) = delete; std::list children; - mio::mmap_source::const_iterator propsBegin; - mio::mmap_source::const_iterator propsEnd; - uint8_t type; + mio::mmap_source::const_iterator propsBegin {}; + mio::mmap_source::const_iterator propsEnd {}; + uint8_t type {}; enum NodeChar : uint8_t { ESCAPE = 0xFD, START = 0xFE, @@ -67,12 +67,22 @@ class PropStream { template bool read(T &ret) { + static_assert(std::is_trivially_copyable_v, "Type T must be trivially copyable"); + if (size() < sizeof(T)) { return false; } - memcpy(&ret, p, sizeof(T)); + std::span charSpan { p, sizeof(T) }; + auto byteSpan = std::as_bytes(charSpan); + + std::array tempBuffer; + std::ranges::copy(byteSpan, tempBuffer.begin()); + + ret = std::bit_cast(tempBuffer); + p += sizeof(T); + return true; } @@ -86,12 +96,14 @@ class PropStream { return false; } - char* str = new char[strLen + 1]; - memcpy(str, p, strLen); - str[strLen] = 0; - ret.assign(str, strLen); - delete[] str; + std::vector tempBuffer(strLen); + std::span sourceSpan(p, strLen); + std::ranges::copy(sourceSpan, tempBuffer.begin()); + + ret.assign(tempBuffer.begin(), tempBuffer.end()); + p += strLen; + return true; } @@ -128,8 +140,11 @@ class PropWriteStream { template void write(T add) { - char* addr = reinterpret_cast(&add); - std::copy(addr, addr + sizeof(T), std::back_inserter(buffer)); + static_assert(std::is_trivially_copyable_v, "Type T must be trivially copyable"); + + auto byteArray = std::bit_cast>(add); + std::span charSpan(byteArray); + std::ranges::copy(charSpan, std::back_inserter(buffer)); } void writeString(const std::string &str) { @@ -140,7 +155,7 @@ class PropWriteStream { } write(static_cast(strLength)); - std::copy(str.begin(), str.end(), std::back_inserter(buffer)); + std::ranges::copy(str, std::back_inserter(buffer)); } private: diff --git a/src/io/filestream.cpp b/src/io/filestream.cpp index 71b2dd4aa2f..687c2501df9 100644 --- a/src/io/filestream.cpp +++ b/src/io/filestream.cpp @@ -17,7 +17,8 @@ uint32_t FileStream::tell() const { void FileStream::seek(uint32_t pos) { if (pos > m_data.size()) { - throw std::ios_base::failure("Seek failed"); + g_logger().error("Seek failed"); + return; } m_pos = pos; } @@ -29,7 +30,8 @@ void FileStream::skip(uint32_t len) { uint32_t FileStream::size() const { std::size_t size = m_data.size(); if (size > std::numeric_limits::max()) { - throw std::overflow_error("File size exceeds uint32_t range"); + g_logger().error("File size exceeds uint32_t range"); + return {}; } return static_cast(size); @@ -37,27 +39,31 @@ uint32_t FileStream::size() const { template bool FileStream::read(T &ret, bool escape) { + static_assert(std::is_trivially_copyable_v, "Type T must be trivially copyable"); + const auto size = sizeof(T); if (m_pos + size > m_data.size()) { - throw std::ios_base::failure("Read failed"); + g_logger().error("Read failed"); + return false; } std::array array; if (escape) { - for (int_fast8_t i = -1; ++i < size;) { + for (int_fast8_t i = 0; i < size; ++i) { if (m_data[m_pos] == OTB::Node::ESCAPE) { ++m_pos; } array[i] = m_data[m_pos]; ++m_pos; } - memcpy(&ret, array.data(), size); } else { - memcpy(&ret, &m_data[m_pos], size); + std::span sourceSpan(m_data.data() + m_pos, size); + std::ranges::copy(sourceSpan, array.begin()); m_pos += size; } + ret = std::bit_cast(array); return true; } @@ -65,7 +71,8 @@ uint8_t FileStream::getU8() { uint8_t v = 0; if (m_pos + 1 > m_data.size()) { - throw std::ios_base::failure("Failed to getU8"); + g_logger().error("Failed to getU8"); + return {}; } // Fast Escape Val @@ -101,13 +108,14 @@ std::string FileStream::getString() { std::string str; if (const uint16_t len = getU16(); len > 0 && len < 8192) { if (m_pos + len > m_data.size()) { - throw std::ios_base::failure("[FileStream::getString] - Read failed"); + g_logger().error("[FileStream::getString] - Read failed"); + return {}; } str = { (char*)&m_data[m_pos], len }; m_pos += len; } else if (len != 0) { - throw std::ios_base::failure("[FileStream::getString] - Read failed because string is too big"); + g_logger().error("[FileStream::getString] - Read failed because string is too big"); } return str; } diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index cb24c092ebb..5b2621c1cbf 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -734,10 +734,6 @@ void IOLoginDataLoad::loadPlayerPreyClass(std::shared_ptr player, DBResu query << "SELECT * FROM `player_prey` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { - auto selectedRaceId = result->getNumber("raceid"); - if (selectedRaceId == 0) { - continue; - } auto slot = std::make_unique(static_cast(result->getNumber("slot"))); auto state = static_cast(result->getNumber("state")); if (slot->id == PreySlot_Two && state == PreyDataState_Locked) { @@ -749,7 +745,7 @@ void IOLoginDataLoad::loadPlayerPreyClass(std::shared_ptr player, DBResu } else { slot->state = state; } - slot->selectedRaceId = selectedRaceId; + slot->selectedRaceId = result->getNumber("raceid"); slot->option = static_cast(result->getNumber("option")); slot->bonus = static_cast(result->getNumber("bonus_type")); slot->bonusRarity = static_cast(result->getNumber("bonus_rarity")); @@ -785,10 +781,6 @@ void IOLoginDataLoad::loadPlayerTaskHuntingClass(std::shared_ptr player, query << "SELECT * FROM `player_taskhunt` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { - auto selectedRaceId = result->getNumber("raceid"); - if (selectedRaceId == 0) { - continue; - } auto slot = std::make_unique(static_cast(result->getNumber("slot"))); auto state = static_cast(result->getNumber("state")); if (slot->id == PreySlot_Two && state == PreyTaskDataState_Locked) { @@ -800,7 +792,7 @@ void IOLoginDataLoad::loadPlayerTaskHuntingClass(std::shared_ptr player, } else { slot->state = state; } - slot->selectedRaceId = selectedRaceId; + slot->selectedRaceId = result->getNumber("raceid"); slot->upgrade = result->getNumber("upgrade"); slot->rarity = static_cast(result->getNumber("rarity")); slot->currentKills = result->getNumber("kills"); diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index f320a67ca22..ec8ab71fd07 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -606,7 +606,7 @@ bool IOLoginDataSave::savePlayerPreyClass(std::shared_ptr player) { << slot->freeRerollTimeStamp << ", "; PropWriteStream propPreyStream; - std::ranges::for_each(slot->raceIdList.begin(), slot->raceIdList.end(), [&propPreyStream](uint16_t raceId) { + std::ranges::for_each(slot->raceIdList, [&propPreyStream](uint16_t raceId) { propPreyStream.write(raceId); }); @@ -659,7 +659,7 @@ bool IOLoginDataSave::savePlayerTaskHuntingClass(std::shared_ptr player) query << slot->freeRerollTimeStamp << ", "; PropWriteStream propTaskHuntingStream; - std::ranges::for_each(slot->raceIdList.begin(), slot->raceIdList.end(), [&propTaskHuntingStream](uint16_t raceId) { + std::ranges::for_each(slot->raceIdList, [&propTaskHuntingStream](uint16_t raceId) { propTaskHuntingStream.write(raceId); }); diff --git a/src/io/ioprey.cpp b/src/io/ioprey.cpp index 8dcbfe10e46..b21f442bf67 100644 --- a/src/io/ioprey.cpp +++ b/src/io/ioprey.cpp @@ -65,8 +65,9 @@ void PreySlot::reloadMonsterGrid(std::vector blackList, uint32_t level // Disabling prey system if the server have less then 36 registered monsters on bestiary because: // - Impossible to generate random lists without duplications on slots. // - Stress the server with unnecessary loops. - std::map bestiary = g_game().getBestiaryList(); + const std::map &bestiary = g_game().getBestiaryList(); if (bestiary.size() < 36) { + g_logger().error("[PreySlot::reloadMonsterGrid] - Bestiary size is less than 36, disabling prey system."); return; } @@ -338,7 +339,7 @@ void IOPrey::parsePreyAction(std::shared_ptr player, PreySlot_t slotId, } else if (player->getPreyWithMonster(raceId)) { player->sendMessageDialog("This creature is already selected on another slot."); return; - } else if (!mtype->info.isPreyable) { + } else if (mtype && !mtype->info.isPreyable) { player->sendMessageDialog("This creature can't be select on prey. Please choose another one."); return; } From 2735b8ce1500c5effa33307f99d85e071a8c3246 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 17 Oct 2024 22:03:29 -0300 Subject: [PATCH 7/8] refactor: improve readability and optimize toPosition function (#2928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors the string.toPosition function to improve readability and optimize the code. The main changes include: • Renaming variables to more descriptive names. • Optimized pattern matching by reusing patterns. • Returning nil when no pattern is matched, improving error handling. These changes make the code clearer and more efficient without altering its functionality. --- data-otservbr-global/lib/quests/soul_war.lua | 18 ------------------ data/libs/functions/string.lua | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/data-otservbr-global/lib/quests/soul_war.lua b/data-otservbr-global/lib/quests/soul_war.lua index 668c67f92b9..f5b2d75f5ba 100644 --- a/data-otservbr-global/lib/quests/soul_war.lua +++ b/data-otservbr-global/lib/quests/soul_war.lua @@ -1569,21 +1569,3 @@ function Creature:applyZoneEffect(var, combat, zoneName) return true end - -function string.toPosition(str) - local patterns = { - -- table format - "{%s*x%s*=%s*(%d+)%s*,%s*y%s*=%s*(%d+)%s*,%s*z%s*=%s*(%d+)%s*}", - -- Position format - "Position%s*%((%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*%)", - -- x, y, z format - "(%d+)%s*,%s*(%d+)%s*,%s*(%d+)", - } - - for _, pattern in ipairs(patterns) do - local x, y, z = string.match(str, pattern) - if x and y and z then - return Position(tonumber(x), tonumber(y), tonumber(z)) - end - end -end diff --git a/data/libs/functions/string.lua b/data/libs/functions/string.lua index 9d746b82e3a..42b7a8bab4b 100644 --- a/data/libs/functions/string.lua +++ b/data/libs/functions/string.lua @@ -129,3 +129,19 @@ end string.capitalize = function(str) return str:gsub("%f[%a].", string.upper) end + +function string.toPosition(inputString) + local positionPatterns = { + "{%s*x%s*=%s*(%d+)%s*,%s*y%s*=%s*(%d+)%s*,%s*z%s*=%s*(%d+)%s*}", + "Position%s*%((%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*%)", + "(%d+)%s*,%s*(%d+)%s*,%s*(%d+)", + } + + for _, pattern in ipairs(positionPatterns) do + local posX, posY, posZ = string.match(inputString, pattern) + if posX and posY and posZ then + return Position(tonumber(posX), tonumber(posY), tonumber(posZ)) + end + end + return nil +end From 1af76e2e04951fe048da5d319c5c3e57a1dc5d6e Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 17 Oct 2024 22:06:04 -0300 Subject: [PATCH 8/8] refactor: optimize time formatting function for better performance (#2904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors the getTimeInWords function to improve performance and code readability. The new implementation adopts a more efficient approach for calculating and formatting time in days, hours, minutes, and seconds, reducing redundancy and simplifying the logical flow. • Optimized the function for better execution speed. • Simplified the conditions and formatting structure. • Maintained the same functionality with clearer and more efficient code. --- .../movements_boss_entrance.lua | 2 +- .../actions_portal_brain_head.lua | 2 +- data/events/scripts/player.lua | 2 +- data/libs/functions/boss_lever.lua | 2 +- data/libs/functions/functions.lua | 62 ------------------- data/libs/functions/game.lua | 34 ++++++++++ data/libs/systems/concoctions.lua | 6 +- .../scripts/eventcallbacks/player/on_look.lua | 2 +- 8 files changed, 42 insertions(+), 70 deletions(-) diff --git a/data-otservbr-global/scripts/quests/dangerous_depth/movements_boss_entrance.lua b/data-otservbr-global/scripts/quests/dangerous_depth/movements_boss_entrance.lua index 20300f3ac5f..a33dccd762d 100644 --- a/data-otservbr-global/scripts/quests/dangerous_depth/movements_boss_entrance.lua +++ b/data-otservbr-global/scripts/quests/dangerous_depth/movements_boss_entrance.lua @@ -37,7 +37,7 @@ function bossEntrance.onStepIn(creature, item, position, fromPosition, toPositio if timeLeft > 0 then player:teleportTo(fromPosition, true) player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have to wait " .. getTimeInWords(timeLeft) .. " to face " .. bossName .. " again!") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have to wait " .. Game.getTimeInWords(timeLeft) .. " to face " .. bossName .. " again!") player:getPosition():sendMagicEffect(CONST_ME_POFF) return true end diff --git a/data-otservbr-global/scripts/quests/feaster_of_souls/actions_portal_brain_head.lua b/data-otservbr-global/scripts/quests/feaster_of_souls/actions_portal_brain_head.lua index aebc0b3ca7a..badaffad5a2 100644 --- a/data-otservbr-global/scripts/quests/feaster_of_souls/actions_portal_brain_head.lua +++ b/data-otservbr-global/scripts/quests/feaster_of_souls/actions_portal_brain_head.lua @@ -124,7 +124,7 @@ function teleportBoss.onStepIn(creature, item, position, fromPosition) if timeLeft > 0 then player:teleportTo(config.exitPosition, true) player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have to wait " .. getTimeInWords(timeLeft) .. " to face " .. config.bossName .. " again!") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have to wait " .. Game.getTimeInWords(timeLeft) .. " to face " .. config.bossName .. " again!") player:getPosition():sendMagicEffect(CONST_ME_POFF) return false end diff --git a/data/events/scripts/player.lua b/data/events/scripts/player.lua index ec1a92f3528..eee28b632d6 100644 --- a/data/events/scripts/player.lua +++ b/data/events/scripts/player.lua @@ -216,7 +216,7 @@ function Player:onLookInBattleList(creature, distance) if master and table.contains(summons, creature:getName():lower()) then local familiarSummonTime = master:kv():get("familiar-summon-time") or 0 description = description .. " (Master: " .. master:getName() .. "). \z - It will disappear in " .. getTimeInWords(familiarSummonTime - os.time()) + It will disappear in " .. Game.getTimeInWords(familiarSummonTime - os.time()) end end if self:getGroup():getAccess() then diff --git a/data/libs/functions/boss_lever.lua b/data/libs/functions/boss_lever.lua index 9cd577ee911..b1141619ce2 100644 --- a/data/libs/functions/boss_lever.lua +++ b/data/libs/functions/boss_lever.lua @@ -191,7 +191,7 @@ function BossLever:onUse(player) local currentTime = os.time() if lastEncounter and currentTime < lastEncounter then local timeLeft = lastEncounter - currentTime - local timeMessage = getTimeInWords(timeLeft) .. " to face " .. self.name .. " again!" + local timeMessage = Game.getTimeInWords(timeLeft) .. " to face " .. self.name .. " again!" local message = "You have to wait " .. timeMessage if currentPlayer ~= player then diff --git a/data/libs/functions/functions.lua b/data/libs/functions/functions.lua index c2349ce7fd6..2327a1cf937 100644 --- a/data/libs/functions/functions.lua +++ b/data/libs/functions/functions.lua @@ -65,43 +65,6 @@ function getTitle(uid) return false end -function getTimeInWords(secsParam) - local secs = tonumber(secsParam) - local days = math.floor(secs / (24 * 3600)) - secs = secs - (days * 24 * 3600) - local hours, minutes, seconds = getHours(secs), getMinutes(secs), getSeconds(secs) - local timeStr = "" - - if days > 0 then - timeStr = days .. (days > 1 and " days" or " day") - end - - if hours > 0 then - if timeStr ~= "" then - timeStr = timeStr .. ", " - end - - timeStr = timeStr .. hours .. (hours > 1 and " hours" or " hour") - end - - if minutes > 0 then - if timeStr ~= "" then - timeStr = timeStr .. ", " - end - - timeStr = timeStr .. minutes .. (minutes > 1 and " minutes" or " minute") - end - - if seconds > 0 then - if timeStr ~= "" then - timeStr = timeStr .. " and " - end - - timeStr = timeStr .. seconds .. (seconds > 1 and " seconds" or " second") - end - return timeStr -end - function getLootRandom(modifier) local multi = (configManager.getNumber(configKeys.RATE_LOOT) * SCHEDULE_LOOT_RATE) * (modifier or 1) return math.random(0, MAX_LOOTCHANCE) * 100 / math.max(1, multi) @@ -949,31 +912,6 @@ function SetInfluenced(monsterType, monster, player, influencedLevel) monster:setForgeStack(influencedLevel) end -function getHours(seconds) - return math.floor((seconds / 60) / 60) -end - -function getMinutes(seconds) - return math.floor(seconds / 60) % 60 -end - -function getSeconds(seconds) - return seconds % 60 -end - -function getTime(seconds) - local hours, minutes = getHours(seconds), getMinutes(seconds) - if minutes > 59 then - minutes = minutes - hours * 60 - end - - if minutes < 10 then - minutes = "0" .. minutes - end - - return hours .. ":" .. minutes .. "h" -end - function ReloadDataEvent(cid) local player = Player(cid) if not player then diff --git a/data/libs/functions/game.lua b/data/libs/functions/game.lua index e4d40bef318..a7c7f7617ce 100644 --- a/data/libs/functions/game.lua +++ b/data/libs/functions/game.lua @@ -133,3 +133,37 @@ function Game.setStorageValue(key, value) globalStorageTable[key] = value end + +function Game.getTimeInWords(seconds) + local days = math.floor(seconds / (24 * 3600)) + seconds = seconds % (24 * 3600) + local hours = math.floor(seconds / 3600) + seconds = seconds % 3600 + local minutes = math.floor(seconds / 60) + seconds = seconds % 60 + + local timeParts = {} + + if days > 0 then + table.insert(timeParts, days .. (days > 1 and " days" or " day")) + end + + if hours > 0 then + table.insert(timeParts, hours .. (hours > 1 and " hours" or " hour")) + end + + if minutes > 0 then + table.insert(timeParts, minutes .. (minutes > 1 and " minutes" or " minute")) + end + + if seconds > 0 or #timeParts == 0 then + table.insert(timeParts, seconds .. (seconds > 1 and " seconds" or " second")) + end + + local timeStr = table.concat(timeParts, ", ") + local lastComma = timeStr:find(", [%a%d]+$") + if lastComma then + timeStr = timeStr:sub(1, lastComma - 1) .. " and" .. timeStr:sub(lastComma + 1) + end + return timeStr +end diff --git a/data/libs/systems/concoctions.lua b/data/libs/systems/concoctions.lua index 72547a8a1ab..ee856e16482 100644 --- a/data/libs/systems/concoctions.lua +++ b/data/libs/systems/concoctions.lua @@ -158,7 +158,7 @@ function Concoction:init(player, sendMessage) return end eventPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your concoction " .. name .. " is still active for another " .. duration .. ".") - end, 500, player:getId(), self.name, getTimeInWords(self:timeLeft(player))) + end, 500, player:getId(), self.name, Game.getTimeInWords(self:timeLeft(player))) end end @@ -180,7 +180,7 @@ function Concoction:activate(player, item) local cooldown = self:cooldown() if self:lastActivatedAt(player) + cooldown > os.time() then local cooldownLeft = self:lastActivatedAt(player) + cooldown - os.time() - player:sendTextMessage(MESSAGE_FAILURE, "You must wait " .. getTimeInWords(cooldownLeft) .. " before using " .. item:getName() .. " again.") + player:sendTextMessage(MESSAGE_FAILURE, "You must wait " .. Game.getTimeInWords(cooldownLeft) .. " before using " .. item:getName() .. " again.") return true end self:timeLeft(player, self:totalDuration()) @@ -191,7 +191,7 @@ function Concoction:activate(player, item) self.config.callback(player, self.config) else self:addCondition(player) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have activated " .. item:getName() .. ". It will last for " .. getTimeInWords(self:totalDuration()) .. consumptionString .. ".") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have activated " .. item:getName() .. ". It will last for " .. Game.getTimeInWords(self:totalDuration()) .. consumptionString .. ".") if self:tickType() == ConcoctionTickType.Online then addEvent(tick, updateInterval * 1000, self.id, player:getId(), updateInterval) end diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 68824cb9b44..e0a1ab86788 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -44,7 +44,7 @@ local function handleCreatureDescription(inspectedThing, lookDistance) local monsterMaster = inspectedThing:getMaster() if monsterMaster and table.contains({ "sorcerer familiar", "knight familiar", "druid familiar", "paladin familiar" }, inspectedThing:getName():lower()) then local summonTimeRemaining = monsterMaster:kv():get("familiar-summon-time") or 0 - descriptionText = string.format("%s (Master: %s). It will disappear in %s", descriptionText, monsterMaster:getName(), getTimeInWords(summonTimeRemaining - os.time())) + descriptionText = string.format("%s (Master: %s). It will disappear in %s", descriptionText, monsterMaster:getName(), Game.getTimeInWords(summonTimeRemaining - os.time())) end end