diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index 3293c5f65..eea62fa57 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 ec7dd9522..165f1e9b1 100644 --- a/.gitignore +++ b/.gitignore @@ -401,5 +401,8 @@ otxserver.old # VCPKG vcpkg_installed +# DB Backups +database_backup + # CLION cmake-build-* diff --git a/README.md b/README.md index 801a1b39a..c1a5bbf23 100644 --- a/README.md +++ b/README.md @@ -12,31 +12,25 @@ We are trying to create the perfect custom OpenTibia server. We currently provide build instructions for the following systems: * [Windows Tutorial for OTX 6](https://github.com/mattyx14/otxserver/wiki/Compiling-on-Windows-(OTX-6-)) -## OpenTibia Comunity: -With downloads, support, tutorials, Lua scripts, C++ codes, PHP codes and more ... -* [OTServ Brasil](https://docs.opentibiabr.com/) - [Português/English] -* [Tibiaking](https://tibiaking.com/) - [Português] -* [OTLand](https://otland.net/) - [English] -* [Tibia Face](https://tibiaface.foroactivo.com/) - [Español] - ## Old Server Series: Server created with TFS 0.3.7-(AKA 0.4) with endless improvements that make it ideal for old school servers based on 8.6, clearly it should be used completely with its data defined there. * [OTX Server 2](https://github.com/mattyx14/otxserver/tree/otxserv2) +## OpenTibia Comunity: +With downloads, support, tutorials, Lua scripts, C++ codes, PHP codes and more ... +* [OTServ Brasil](https://docs.opentibiabr.com/) - [Português/English] +* [Tibiaking](https://tibiaking.com/) - [Português] +* [OTLand](https://otland.net/) - [English] +* [Tibia Face](https://tibiaface.foroactivo.com/) - [Español] + ## Contacts OTX Server 2: - Matty(English & Spanish):
Facebook: https://www.facebook.com/Mattyx14/
E-mail: darkylive@live.com.mx
-Whatsapp: +5213173832937
- +Whatsapp: +523211136700

- Reason(English & Portuguese):
Discord: Reason#2913 -- FeeTads(Portuguese):
-E-mail: felps18.082@gmail.com
-Whatsapp: +55 41 9 84036942
-Discord: FeeTads#0246 - ## Special Thanks - Our contributors ([Canary](https://github.com/opentibiabr/canary/graphs/contributors) | [OTServBR-Global](https://github.com/opentibiabr/otservbr-global/graphs/contributors)). diff --git a/config.lua.dist b/config.lua.dist index 735977dc8..410057f15 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -26,6 +26,7 @@ maintainModeMessage = "" -- NOTE: valid values for worldType are: "pvp", "no-pvp" and "pvp-enforced" -- NOTE: removeBeginningWeaponAmmunition: spears, arrows, bolt have endless ammo (allows training for paladins) -- NOTE: refundManaOnBeginningWeapons: wand of vortex and snakebite refund mana used (allows training for mages) +-- NOTE: loginProtectionTime in MS worldType = "pvp" hotkeyAimbotEnabled = true protectionLevel = 7 @@ -47,6 +48,7 @@ monthKillsToRedSkull = 10 redSkullDuration = 1 blackSkullDuration = 3 orangeSkullDuration = 7 +loginProtectionTime = 10 * 1000 cleanProtectionZones = false @@ -401,6 +403,7 @@ mysqlHost = "127.0.0.1" mysqlUser = "root" mysqlPass = "" mysqlDatabase = "" +mysqlDatabaseBackup = true mysqlPort = 3306 mysqlSock = "" passwordType = "sha1" diff --git a/data-otxserver/lib/functions/load.lua b/data-otxserver/lib/functions/load.lua new file mode 100644 index 000000000..9862b00d2 --- /dev/null +++ b/data-otxserver/lib/functions/load.lua @@ -0,0 +1 @@ +dofile(DATA_DIRECTORY .. "/lib/functions/players.lua") diff --git a/data-otxserver/lib/functions/players.lua b/data-otxserver/lib/functions/players.lua new file mode 100644 index 000000000..45fec4c66 --- /dev/null +++ b/data-otxserver/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-otxserver/lib/lib.lua b/data-otxserver/lib/lib.lua index 2e9ead889..e6ee85db8 100644 --- a/data-otxserver/lib/lib.lua +++ b/data-otxserver/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-otxserver/npc/hireling.lua b/data-otxserver/npc/hireling.lua index bfc9000ca..234c28012 100644 --- a/data-otxserver/npc/hireling.lua +++ b/data-otxserver/npc/hireling.lua @@ -233,7 +233,7 @@ function createHirelingType(HirelingName) { itemName = "crossbow", clientId = 3349, buy = 500, sell = 120 }, { itemName = "crystalline arrow", clientId = 15793, buy = 450 }, { itemName = "drill bolt", clientId = 16142, buy = 12 }, - { itemName = "diamond arrow", clientId = 35901, buy = 100 }, + { itemName = "diamond arrow", clientId = 35901, buy = 130 }, { itemName = "earth arrow", clientId = 774, buy = 5 }, { itemName = "envenomed arrow", clientId = 16143, buy = 12 }, { itemName = "flaming arrow", clientId = 763, buy = 5 }, @@ -276,16 +276,16 @@ function createHirelingType(HirelingName) }, ["potions"] = { { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "great mana potion", clientId = 238, buy = 158 }, + { itemName = "great spirit potion", clientId = 7642, buy = 254 }, { itemName = "health potion", clientId = 266, buy = 50 }, { itemName = "mana potion", clientId = 268, buy = 56 }, { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, + { itemName = "strong mana potion", clientId = 237, buy = 108 }, + { itemName = "supreme health potion", clientId = 23375, buy = 650 }, { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, - { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 488 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 488 }, { itemName = "empty potion flask", clientId = 283, sell = 5 }, { itemName = "empty potion flask", clientId = 284, sell = 5 }, { itemName = "empty potion flask", clientId = 285, sell = 5 }, @@ -293,7 +293,7 @@ function createHirelingType(HirelingName) }, ["runes"] = { { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "avalanche rune", clientId = 3161, buy = 64 }, { itemName = "blank rune", clientId = 3147, buy = 10 }, { itemName = "chameleon rune", clientId = 3178, buy = 210 }, { itemName = "convince creature rune", clientId = 3177, buy = 80 }, @@ -308,7 +308,7 @@ function createHirelingType(HirelingName) { itemName = "fire field rune", clientId = 3188, buy = 28 }, { itemName = "fire wall rune", clientId = 3190, buy = 61 }, { itemName = "fireball rune", clientId = 3189, buy = 30 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "great fireball rune", clientId = 3191, buy = 64 }, { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, { itemName = "holy missile rune", clientId = 3182, buy = 16 }, { itemName = "icicle rune", clientId = 3158, buy = 30 }, @@ -321,9 +321,9 @@ function createHirelingType(HirelingName) { itemName = "poison wall rune", clientId = 3176, buy = 52 }, { itemName = "soulfire rune", clientId = 3195, buy = 46 }, { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "stone shower rune", clientId = 3175, buy = 37 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "stone shower rune", clientId = 3175, buy = 41 }, + { itemName = "sudden death rune", clientId = 3155, buy = 162 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 52 }, { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, { itemName = "wild growth rune", clientId = 3156, buy = 160 }, }, diff --git a/data-otxserver/scripts/creaturescripts/customs/freequests.lua b/data-otxserver/scripts/creaturescripts/customs/freequests.lua index 3a429715a..c6756005f 100644 --- a/data-otxserver/scripts/creaturescripts/customs/freequests.lua +++ b/data-otxserver/scripts/creaturescripts/customs/freequests.lua @@ -18,8 +18,15 @@ local function playerFreeQuestStart(playerId, index) return end - if player:getStorageValue(questTable[index].storage) ~= questTable[index].storageValue then - player:setStorageValue(questTable[index].storage, questTable[index].storageValue) + local questData = questTable[index] + local currentStorageValue = player:getStorageValue(questData.storage) + + if not questData.storage then + logger.warn("[Freequest System]: error storage for '" .. questData.storageName .. "' is nil for the index") + elseif currentStorageValue ~= questData.storageValue then + player:setStorageValue(questData.storage, questData.storageValue) + elseif currentStorageValue == -1 then + logger.warn("[Freequest System]: warning Storage '" .. questData.storageName .. "' currently nil for player ID " .. playerId) end end diff --git a/data/XML/events.xml b/data/XML/events.xml index 6a33c940f..70c45e629 100644 --- a/data/XML/events.xml +++ b/data/XML/events.xml @@ -1,12 +1,12 @@ - +
- + diff --git a/data/XML/imbuements.xml b/data/XML/imbuements.xml index 0a2fd9070..fa1c3312d 100644 --- a/data/XML/imbuements.xml +++ b/data/XML/imbuements.xml @@ -1,7 +1,7 @@ - - + + diff --git a/data/events/scripts/creature.lua b/data/events/scripts/creature.lua index e7a74a331..639fe52ca 100644 --- a/data/events/scripts/creature.lua +++ b/data/events/scripts/creature.lua @@ -1,60 +1,8 @@ -local function removeCombatProtection(playerUid) - local player = Player(playerUid) - if not player then - return true - end - - local time = 0 - if player:isMage() then - time = 10 - elseif player:isPaladin() then - time = 20 - else - time = 30 - end - - player:kv():set("combat-protection", 2) - addEvent(function(playerFuncUid) - local playerEvent = Player(playerFuncUid) - if not playerEvent then - return - end - - playerEvent:kv():remove("combat-protection") - playerEvent:remove() - end, time * 1000, playerUid) -end - function Creature:onTargetCombat(target) if not self then return true end - if target:isPlayer() then - if self:isMonster() then - local isProtected = target:kv():get("combat-protection") or 0 - - if target:getIp() == 0 then -- If player is disconnected, monster shall ignore to attack the player - if target:isPzLocked() then - return true - end - if isProtected <= 0 then - addEvent(removeCombatProtection, 30 * 1000, target.uid) - target:kv():set("combat-protection", 1) - elseif isProtected == 1 then - self:searchTarget() - return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER - end - - return true - end - - if isProtected >= os.time() then - return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER - end - end - end - if (target:isMonster() and self:isPlayer() and target:getMaster() == self) or (self:isMonster() and target:isPlayer() and self:getMaster() == target) then return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE end diff --git a/data/libs/functions/player.lua b/data/libs/functions/player.lua index 6adf78056..158d55b1f 100644 --- a/data/libs/functions/player.lua +++ b/data/libs/functions/player.lua @@ -89,24 +89,6 @@ function Player.addManaSpent(...) return ret 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 diff --git a/data/scripts/creaturescripts/player/login.lua b/data/scripts/creaturescripts/player/login.lua index fe52be71b..2f33879ac 100644 --- a/data/scripts/creaturescripts/player/login.lua +++ b/data/scripts/creaturescripts/player/login.lua @@ -2,21 +2,6 @@ local function sendBoostMessage(player, category, isIncreased) return player:sendTextMessage(MESSAGE_BOOSTED_CREATURE, string.format("Event! %s is %screased. Happy Hunting!", category, isIncreased and "in" or "de")) end -local function onMovementRemoveProtection(playerId, oldPos, time) - local player = Player(playerId) - if not player then - return true - end - - local playerPos = player:getPosition() - if (playerPos.x ~= oldPos.x or playerPos.y ~= oldPos.y or playerPos.z ~= oldPos.z) or player:getTarget() then - player:kv():remove("combat-protection") - return true - end - - addEvent(onMovementRemoveProtection, 1000, playerId, oldPos, time - 1) -end - local playerLoginGlobal = CreatureEvent("PlayerLoginGlobal") function playerLoginGlobal.onLogin(player) @@ -162,13 +147,6 @@ function playerLoginGlobal.onLogin(player) player:setRemoveBossTime(1) end - -- Remove combat protection - local isProtected = player:kv():get("combat-protection") or 0 - if isProtected < 1 then - player:kv():set("combat-protection", 1) - onMovementRemoveProtection(playerId, player:getPosition(), 10) - end - -- Change support outfit to a normal outfit to open customize character without crashes local playerOutfit = player:getOutfit() if table.contains({ 75, 266, 302 }, playerOutfit.lookType) then diff --git a/data/scripts/lib/shops.lua b/data/scripts/lib/shops.lua index af19fd803..8c3893493 100644 --- a/data/scripts/lib/shops.lua +++ b/data/scripts/lib/shops.lua @@ -4,31 +4,31 @@ SupplyShopConfigTable = { { itemName = "fire mushroom", clientId = 3731, buy = 300 }, }, ["exercise weapons"] = { - { itemName = "enhanced exercise axe", clientId = 35280, buy = 2340000 }, - { itemName = "enhanced exercise bow", clientId = 35282, buy = 2340000 }, - { itemName = "enhanced exercise club", clientId = 35281, buy = 2340000 }, - { itemName = "enhanced exercise rod", clientId = 35283, buy = 2340000 }, - { itemName = "enhanced exercise shield", clientId = 44066, buy = 2340000 }, - { itemName = "enhanced exercise sword", clientId = 35279, buy = 2340000 }, - { itemName = "enhanced exercise wand", clientId = 35284, buy = 2340000 }, - { itemName = "exercise axe", clientId = 28553, buy = 1800000 }, - { itemName = "exercise bow", clientId = 28555, buy = 1800000 }, - { itemName = "exercise club", clientId = 28554, buy = 1800000 }, - { itemName = "exercise rod", clientId = 28556, buy = 1800000 }, - { itemName = "exercise shield", clientId = 44065, buy = 1800000 }, - { itemName = "exercise sword", clientId = 28552, buy = 1800000 }, - { itemName = "exercise wand", clientId = 28557, buy = 1800000 }, - { itemName = "masterful exercise axe", clientId = 35286, buy = 2700000 }, - { itemName = "masterful exercise bow", clientId = 35288, buy = 2700000 }, - { itemName = "masterful exercise club", clientId = 35287, buy = 2700000 }, - { itemName = "masterful exercise rod", clientId = 35289, buy = 2700000 }, - { itemName = "masterful exercise shield", clientId = 44067, buy = 2700000 }, - { itemName = "masterful exercise sword", clientId = 35285, buy = 2700000 }, - { itemName = "masterful exercise wand", clientId = 35290, buy = 2700000 }, + { itemName = "durable exercise axe", clientId = 35280, buy = 1250000 }, + { itemName = "durable exercise bow", clientId = 35282, buy = 1250000 }, + { itemName = "durable exercise club", clientId = 35281, buy = 1250000 }, + { itemName = "durable exercise rod", clientId = 35283, buy = 1250000 }, + { itemName = "durable exercise shield", clientId = 44066, buy = 1250000 }, + { itemName = "durable exercise sword", clientId = 35279, buy = 1250000 }, + { itemName = "durable exercise wand", clientId = 35284, buy = 1250000 }, + { itemName = "exercise axe", clientId = 28553, buy = 347222 }, + { itemName = "exercise bow", clientId = 28555, buy = 347222 }, + { itemName = "exercise club", clientId = 28554, buy = 347222 }, + { itemName = "exercise rod", clientId = 28556, buy = 347222 }, + { itemName = "exercise shield", clientId = 44065, buy = 347222 }, + { itemName = "exercise sword", clientId = 28552, buy = 347222 }, + { itemName = "exercise wand", clientId = 28557, buy = 347222 }, + { itemName = "lasting exercise axe", clientId = 35286, buy = 10000000 }, + { itemName = "lasting exercise bow", clientId = 35288, buy = 10000000 }, + { itemName = "lasting exercise club", clientId = 35287, buy = 10000000 }, + { itemName = "lasting exercise rod", clientId = 35289, buy = 10000000 }, + { itemName = "lasting exercise shield", clientId = 44067, buy = 10000000 }, + { itemName = "lasting exercise sword", clientId = 35285, buy = 10000000 }, + { itemName = "lasting exercise wand", clientId = 35290, buy = 10000000 }, }, ["distance equipments"] = { { itemName = "envenomed arrow", clientId = 16143, buy = 12 }, - { itemName = "diamond arrow", clientId = 35901, buy = 100 }, + { itemName = "diamond arrow", clientId = 35901, buy = 130 }, { itemName = "drill bolt", clientId = 16142, buy = 12 }, { itemName = "crystalline arrow", clientId = 15793, buy = 20 }, { itemName = "blue quiver", clientId = 35848, buy = 400 }, @@ -58,7 +58,7 @@ SupplyShopConfigTable = { }, ["runes"] = { { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "avalanche rune", clientId = 3161, buy = 64 }, { itemName = "blank rune", clientId = 3147, buy = 10 }, { itemName = "chameleon rune", clientId = 3178, buy = 210 }, { itemName = "convince creature rune", clientId = 3177, buy = 80 }, @@ -74,7 +74,7 @@ SupplyShopConfigTable = { { itemName = "fire field rune", clientId = 3188, buy = 28 }, { itemName = "fire wall rune", clientId = 3190, buy = 61 }, { itemName = "fireball rune", clientId = 3189, buy = 65 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "great fireball rune", clientId = 3191, buy = 64 }, { itemName = "heavy magic missile rune", clientId = 3198, buy = 65 }, { itemName = "holy missile rune", clientId = 3182, buy = 16 }, { itemName = "icicle rune", clientId = 3158, buy = 65 }, @@ -87,9 +87,9 @@ SupplyShopConfigTable = { { itemName = "poison wall rune", clientId = 3176, buy = 52 }, { itemName = "soulfire rune", clientId = 3195, buy = 46 }, { itemName = "stalagmite rune", clientId = 3179, buy = 65 }, - { itemName = "stone shower rune", clientId = 3175, buy = 57 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 57 }, + { itemName = "stone shower rune", clientId = 3175, buy = 41 }, + { itemName = "sudden death rune", clientId = 3155, buy = 162 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 52 }, { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, { itemName = "wild growth rune", clientId = 3156, buy = 160 }, }, @@ -110,7 +110,7 @@ SupplyShopConfigTable = { { itemName = "prismatic necklace", clientId = 16113, buy = 20000 }, { itemName = "sacred tree amulet", clientId = 9302, buy = 30000 }, { itemName = "shockwave amulet", clientId = 9304, buy = 30000 }, - { itemName = "stone skin amulet", clientId = 3081, buy = 5000 }, + { itemName = "stone skin amulet", clientId = 3081, buy = 25000 }, { itemName = "collar of blue plasma", clientId = 23542, buy = 60000 }, { itemName = "collar of green plasma", clientId = 23543, buy = 60000 }, { itemName = "collar of red plasma", clientId = 23544, buy = 60000 }, @@ -118,7 +118,7 @@ SupplyShopConfigTable = { }, ["rings"] = { { itemName = "life ring", clientId = 3052, buy = 900 }, - { itemName = "might ring", clientId = 3048, buy = 5000 }, + { itemName = "might ring", clientId = 3048, buy = 25000 }, { itemName = "ring of blue plasma", clientId = 23529, buy = 80000 }, { itemName = "ring of green plasma", clientId = 23531, buy = 80000 }, { itemName = "ring of healing", clientId = 3098, buy = 2000 }, @@ -131,17 +131,17 @@ SupplyShopConfigTable = { }, ["potions"] = { { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "great mana potion", clientId = 238, buy = 158 }, + { itemName = "great spirit potion", clientId = 7642, buy = 254 }, { itemName = "health potion", clientId = 266, buy = 50 }, { itemName = "mana potion", clientId = 268, buy = 56 }, { itemName = "mana shield potion", clientId = 35563, buy = 200000 }, { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, - { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 488 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 488 }, + { itemName = "supreme health potion", clientId = 23375, buy = 650 }, { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "strong mana potion", clientId = 237, buy = 108 }, }, } diff --git a/data/scripts/movements/closing_door.lua b/data/scripts/movements/closing_door.lua index 3a6340726..1f977deb4 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 diff --git a/data/scripts/spells/attack/annihilation.lua b/data/scripts/spells/attack/annihilation.lua index 5b9f7bc7f..9bc65f676 100644 --- a/data/scripts/spells/attack/annihilation.lua +++ b/data/scripts/spells/attack/annihilation.lua @@ -33,7 +33,7 @@ spell:needTarget(true) spell:blockWalls(true) spell:needWeapon(true) spell:cooldown(30 * 1000) -spell:groupCooldown(4 * 1000) +spell:groupCooldown(2 * 1000) spell:needLearn(false) spell:vocation("knight;true", "elite knight;true") spell:register() diff --git a/data/scripts/spells/attack/ultimate_ice_strike.lua b/data/scripts/spells/attack/ultimate_ice_strike.lua index 4f8560248..ec494cb7b 100644 --- a/data/scripts/spells/attack/ultimate_ice_strike.lua +++ b/data/scripts/spells/attack/ultimate_ice_strike.lua @@ -30,7 +30,7 @@ spell:range(3) spell:needCasterTargetOrDirection(true) spell:blockWalls(true) spell:cooldown(30 * 1000) -spell:groupCooldown(4 * 1000, 30 * 1000) +spell:groupCooldown(2 * 1000, 30 * 1000) spell:needLearn(false) spell:vocation("druid;true", "elder druid;true") spell:register() diff --git a/data/scripts/spells/attack/ultimate_terra_strike.lua b/data/scripts/spells/attack/ultimate_terra_strike.lua index 705571c59..d7309183f 100644 --- a/data/scripts/spells/attack/ultimate_terra_strike.lua +++ b/data/scripts/spells/attack/ultimate_terra_strike.lua @@ -30,7 +30,7 @@ spell:range(3) spell:needCasterTargetOrDirection(true) spell:blockWalls(true) spell:cooldown(30 * 1000) -spell:groupCooldown(4 * 1000, 30 * 1000) +spell:groupCooldown(2 * 1000, 30 * 1000) spell:needLearn(false) spell:vocation("druid;true", "elder druid;true") spell:register() diff --git a/docs/python-scripts/modify_lua_script_interfaces.py b/docs/python-scripts/modify_lua_script_interfaces.py new file mode 100644 index 000000000..881a8453d --- /dev/null +++ b/docs/python-scripts/modify_lua_script_interfaces.py @@ -0,0 +1,211 @@ +import os +import re +import argparse + +# Functions that modify the file + +def remove_constructor_and_destructor(content): + # Remove the class constructor and destructor, including a blank line after the destructor + pattern = r"explicit\s+\w+\(lua_State\* L\)\s*:\s*LuaScriptInterface\(\"[^\"]+\"\)\s*\{[^\}]*\}\n\s*~\w+\(\)\s*override\s*=\s*default;\n\s*\n?" + content = re.sub(pattern, "", content, flags=re.DOTALL) + return content + +def remove_include_luascript(content): + # Remove the specified include and the blank line below it + pattern = r'#include\s+"lua/scripts/luascript.hpp"\n\n?' + content = re.sub(pattern, "", content) + return content + +def remove_final_luascriptinterface(content): + # Remove "final : LuaScriptInterface", "public LuaScriptInterface", "final :", and "LuaScriptInterface" alone + pattern = r'final\s*:\s*public\s+LuaScriptInterface|public\s+LuaScriptInterface|final\s*:\s*|LuaScriptInterface\s*' + content = re.sub(pattern, "", content) + # Remove extra spaces between the class name and the opening block + content = re.sub(r'class\s+(\w+)\s*\{', r'class \1 {', content) + return content + +def move_init_function_to_cpp(hpp_content, cpp_content, class_name): + # Extracts the init function from the hpp, keeping the signature + pattern = r'static void init\(lua_State\* L\)\s*\{[^\}]*\}' + match = re.search(pattern, hpp_content, flags=re.DOTALL) + if match: + # Keep the function signature in the hpp, but remove the body + init_function_signature = "static void init(lua_State* L);" + hpp_content = re.sub(pattern, init_function_signature, hpp_content, flags=re.DOTALL) + + # Adjust the function signature for the cpp + init_function = match.group() + init_function = re.sub(r'static void\s+', f'void {class_name}::', init_function) + # Remove extra indentation + init_function = init_function.replace(' \n\t\t', '\n\t') + # Remove the extra tab from the function closure + init_function = init_function.replace('\n\t}', '\n}') + + # Add a blank line before and after the function + init_function = f"\n{init_function}\n" + + # Add the function to the beginning of the cpp, after the includes + last_include = re.findall(r'#include\s+<[^>]+>|#include\s+"[^"]+"', cpp_content) + if last_include: + last_include_pos = cpp_content.rfind(last_include[-1]) + len(last_include[-1]) + cpp_content = cpp_content[:last_include_pos] + "\n" + init_function + cpp_content[last_include_pos:] + else: + cpp_content = init_function + cpp_content + return hpp_content, cpp_content + +def add_include_to_cpp(cpp_content): + # Add the new include after the last include, if it is not already present + include_statement = '#include "lua/functions/lua_functions_loader.hpp"' + if include_statement not in cpp_content: + # Locate the last include + last_include = re.findall(r'#include\s+<[^>]+>|#include\s+"[^"]+"', cpp_content) + if last_include: + last_include_pos = cpp_content.rfind(last_include[-1]) + len(last_include[-1]) + # Make sure there are not multiple line breaks before the include + cpp_content = cpp_content[:last_include_pos].rstrip() + "\n" + include_statement + "\n\n" + cpp_content[last_include_pos:].lstrip() + return cpp_content + +def process_files(hpp_file_path, cpp_file_path): + with open(hpp_file_path, 'r', encoding='utf-8') as hpp_file: + hpp_content = hpp_file.read() + + with open(cpp_file_path, 'r', encoding='utf-8') as cpp_file: + cpp_content = cpp_file.read() + + # Get the class name from the hpp file + class_name_match = re.search(r'class\s+(\w+)', hpp_content) + class_name = class_name_match.group(1) if class_name_match else None + + if class_name: + # Apply all modifications + hpp_content = remove_constructor_and_destructor(hpp_content) + hpp_content = remove_include_luascript(hpp_content) + hpp_content = remove_final_luascriptinterface(hpp_content) + hpp_content, cpp_content = move_init_function_to_cpp(hpp_content, cpp_content, class_name) + cpp_content = add_include_to_cpp(cpp_content) + + # Save the modified files + with open(hpp_file_path, 'w', encoding='utf-8') as hpp_file: + hpp_file.write(hpp_content) + + with open(cpp_file_path, 'w', encoding='utf-8') as cpp_file: + cpp_file.write(cpp_content) + + print(f'Modifications applied to: {hpp_file_path} and {cpp_file_path}') + +def main(directory): + # Scan the specified folder and find all .hpp and .cpp files + for root, _, files in os.walk(directory): + for file in files: + if file.endswith('.hpp'): + hpp_file_path = os.path.join(root, file) + cpp_file_path = hpp_file_path.replace('.hpp', '.cpp') + if os.path.exists(cpp_file_path): + process_files(hpp_file_path, cpp_file_path) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Apply modifications to .hpp and .cpp files according to specifications.') + parser.add_argument('directory', type=str, nargs='?', default='../../src/lua/functions/', help='Path of the directory to be scanned.') + args = parser.parse_args() + + main(args.directory) + +# Functions you want to migrate to static calls +functions_to_convert = [ + "getNumber", + "getBoolean", + "getString", + "getUserdata", + "getScriptEnv", + "getCreature", + "getPlayer", + "getPosition", + "getOutfit", + "getThing", + "getUserdataShared", + "getUserdataType", + "getRawUserDataShared", + "getErrorDesc", + "getFormatedLoggerMessage", + "getField", + "getFieldString", + "getVariant", + "getGuild", + "isTable", + "isString", + "isNumber", + "isBoolean", + "isNil", + "isFunction", + "isUserdata", + "reportError", + "reportErrorFunc", + "pushInstantSpell", + "pushVariant", + "pushOutfit", + "pushCylinder", + "pushBoolean", + "pushString", + "pushUserdata", + "pushPosition", + "setMetatable", + "setWeakMetatable", + "setCreatureMetatable", + "setItemMetatable", + "setField", + "registerVariable", + "registerGlobalMethod", + "registerGlobalVariable", + "registerGlobalBoolean", + "registerMethod", + "registerClass", + "registerTable", + "registerSharedClass", + "registerMetaMethod", +] + +# Files you want to exclude from scanning (relative paths) +files_to_exclude = [ + os.path.normpath("lua_functions_loader.cpp"), + os.path.normpath("lua_functions_loader.hpp") +] + +def convert_to_static(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + original_content = content # Keep the original content to check for changes + + for function in functions_to_convert: + # Regex to capture function calls and replace them with Lua::function + # Skip calls that are part of g_configManager() and handle both regular and template functions + pattern = r'(?)?\(' + replacement = rf'Lua::{function}\1(' + content = re.sub(pattern, replacement, content) + + if content != original_content: + with open(file_path, 'w', encoding='utf-8') as file: + file.write(content) + print(f'File converted: {file_path}') + else: + print(f'No changes made to file: {file_path}') + +def main(directory): + # Scan the specified folder and find all .cpp and .hpp files + for root, _, files in os.walk(directory): + for file in files: + if file.endswith(('.cpp', '.hpp')): + file_path = os.path.normpath(os.path.join(root, file)) + + # Check if the file is in the exclusion list + if any(os.path.basename(file_path) == exclude_file for exclude_file in files_to_exclude): + print(f'File ignored: {file_path}') + continue # Skip the specified files + + convert_to_static(file_path) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Convert functions to static calls.') + parser.add_argument('directory', type=str, nargs='?', default='../../src/lua/functions/', help='Path of the directory to be scanned.') + + main(args.directory) diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index b2e8fd807..b46d19417 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/canary_server.cpp b/src/canary_server.cpp index 8dfbcfc95..492028093 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 65e585159..16b912894 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, @@ -180,6 +181,7 @@ enum ConfigKey_t : uint16_t { ONSLAUGHT_CHANCE_FORMULA_C, OPTIMIZE_DATABASE, ORANGE_SKULL_DURATION, + LOGIN_PROTECTION_TIME, OWNER_EMAIL, OWNER_NAME, PARALLELISM, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1034a28be..228b93426 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", ""); @@ -286,6 +287,7 @@ bool ConfigManager::load() { loadIntConfig(L, MONTH_KILLS_TO_RED, "monthKillsToRedSkull", 10); loadIntConfig(L, MULTIPLIER_ATTACKONFIST, "multiplierSpeedOnFist", 5); loadIntConfig(L, ORANGE_SKULL_DURATION, "orangeSkullDuration", 7); + loadIntConfig(L, LOGIN_PROTECTION_TIME, "loginProtectionTime", 10000); loadIntConfig(L, PARALLELISM, "parallelism", 2); loadIntConfig(L, PARTY_LIST_MAX_DISTANCE, "partyListMaxDistance", 0); loadIntConfig(L, PREY_BONUS_REROLL_PRICE, "preyBonusRerollPrice", 1); diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp index da90b03f6..4d4796ec2 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; @@ -1270,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)) { @@ -1769,9 +1775,10 @@ bool ConditionDamage::getNextDamage(int32_t &damage) { } bool ConditionDamage::doDamage(const std::shared_ptr &creature, int32_t healthChange) const { - 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)) { + // Only perform checks and assign attacker if owner is not 0, keeping a const reference to the shared_ptr + const auto &attacker = (owner != 0) ? (g_game().getPlayerByGUID(owner) ? g_game().getPlayerByGUID(owner)->getCreature() : g_game().getCreatureByID(owner)) : nullptr; + const auto &attackerPlayer = attacker ? attacker->getPlayer() : nullptr; + if (creature->isSuppress(getType(), attackerPlayer != nullptr)) { return true; } @@ -1780,7 +1787,7 @@ bool ConditionDamage::doDamage(const std::shared_ptr &creature, int32_ damage.primary.value = healthChange; damage.primary.type = Combat::ConditionToDamageType(conditionType); - if (field && creature->getPlayer() && attacker && attacker->getPlayer()) { + if (field && creature->getPlayer() && attackerPlayer) { damage.primary.value = static_cast(std::round(damage.primary.value / 2.)); } diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 4d02f54c3..2ed8b892e 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -160,6 +160,14 @@ void Creature::onAttacking(uint32_t interval) { return; } + if (attackedCreature->getType() == CreatureType_t::CREATURETYPE_PLAYER) { + const auto &player = attackedCreature->getPlayer(); + if (player && player->isDisconnected() && !player->isProtected()) { + player->setProtection(true); + player->setLoginProtection(30000); + } + } + onAttacked(); attackedCreature->onAttacked(); diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 13b30321a..95412cc67 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; @@ -668,7 +669,8 @@ bool Monster::isOpponent(const std::shared_ptr &creature) const { return creature != master; } - if (creature->getPlayer() && creature->getPlayer()->hasFlag(PlayerFlags_t::IgnoredByMonsters)) { + const auto &player = creature ? creature->getPlayer() : nullptr; + if (player && player->hasFlag(PlayerFlags_t::IgnoredByMonsters)) { return false; } @@ -678,7 +680,7 @@ bool Monster::isOpponent(const std::shared_ptr &creature) const { const auto &creatureMaster = creature->getMaster(); const auto &creaturePlayer = creatureMaster ? creatureMaster->getPlayer() : nullptr; - if (creature->getPlayer() || creaturePlayer) { + if (player || creaturePlayer) { return true; } @@ -912,10 +914,6 @@ bool Monster::isTarget(const std::shared_ptr &creature) { } if (!isSummon()) { - if (creature->getPlayer() && creature->getPlayer()->isDisconnected()) { - return false; - } - if (getFaction() != FACTION_DEFAULT) { return isEnemyFaction(creature->getFaction()); } @@ -933,6 +931,11 @@ bool Monster::selectTarget(const std::shared_ptr &creature) { return false; } + const auto &player = creature ? creature->getPlayer() : nullptr; + if (player && player->isLoginProtected()) { + return false; + } + auto it = getTargetIterator(creature); if (it == targetList.end()) { // Target not found in our target list. @@ -1087,11 +1090,10 @@ void Monster::onThink_async() { setFollowCreature(master); } } else if (!targetList.empty()) { - const bool attackedCreatureIsDisconnected = attackedCreature && attackedCreature->getPlayer() && attackedCreature->getPlayer()->isDisconnected(); const bool attackedCreatureIsUnattackable = attackedCreature && !canUseAttack(getPosition(), attackedCreature); const bool attackedCreatureIsUnreachable = targetDistance <= 1 && attackedCreature && followCreature && !hasFollowPath; - if (!attackedCreature || attackedCreatureIsDisconnected || attackedCreatureIsUnattackable || attackedCreatureIsUnreachable) { - if (!followCreature || !hasFollowPath || attackedCreatureIsDisconnected) { + if (!attackedCreature || attackedCreatureIsUnattackable || attackedCreatureIsUnreachable) { + if (!followCreature || !hasFollowPath) { searchTarget(TARGETSEARCH_NEAREST); } else if (attackedCreature && isFleeing() && !canUseAttack(getPosition(), attackedCreature)) { searchTarget(TARGETSEARCH_DEFAULT); @@ -1114,6 +1116,11 @@ void Monster::doAttacking(uint32_t interval) { return; } + const auto &player = attackedCreature->getPlayer(); + if (player && player->isLoginProtected()) { + return; + } + bool updateLook = true; bool resetTicks = interval != 0; attackTicks += interval; diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 6e44d8f36..3661eea9b 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 e2ab99a1d..65bec5598 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; } @@ -808,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(); diff --git a/src/creatures/npcs/npc.hpp b/src/creatures/npcs/npc.hpp index dc3e16961..47945b79e 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 53ed75733..9abc6be6e 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 b91e1547c..d587a17b9 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/creatures/players/cyclopedia/player_title.cpp b/src/creatures/players/cyclopedia/player_title.cpp index 089d02f28..c7e1c8f04 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/creatures/players/player.cpp b/src/creatures/players/player.cpp index bfbc8148d..afddebb79 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 { @@ -2045,16 +2045,20 @@ void Player::sendPing() { const int64_t noPongTime = timeNow - lastPong; const auto &attackedCreature = getAttackedCreature(); - if ((hasLostConnection || noPongTime >= 7000) && attackedCreature && attackedCreature->getPlayer()) { + if ((hasLostConnection || noPongTime >= 10000) && attackedCreature) { setAttackedCreature(nullptr); } - if (noPongTime >= 60000 && canLogout() && g_creatureEvents().playerLogout(static_self_cast())) { - g_logger().info("Player {} has been kicked due to ping timeout. (has client: {})", getName(), client != nullptr); - if (client) { - client->logout(true, true); + if (noPongTime >= 60000 && shouldForceLogout) { + if (canLogout() && g_creatureEvents().playerLogout(static_self_cast())) { + g_logger().info("Player {} has been kicked due to ping timeout. (has client: {})", getName(), client != nullptr); + if (client) { + client->logout(true, true); + } else { + g_game().removeCreature(static_self_cast(), true); + } } else { - g_game().removeCreature(static_self_cast(), true); + shouldForceLogout = false; } } } @@ -2930,6 +2934,23 @@ bool Player::canDoPotionAction() const { return nextPotionAction <= OTSYS_TIME(); } +void Player::setLoginProtection(int64_t time) { + loginProtectionTime = OTSYS_TIME() + time; +} +bool Player::isLoginProtected() const { + return loginProtectionTime > OTSYS_TIME(); +} +void Player::resetLoginProtection() { + loginProtectionTime = 0; +} + +void Player::setProtection(bool status) { + connProtected = status; +} +bool Player::isProtected() { + return connProtected; +} + void Player::cancelPush() { if (actionTaskEventPush != 0) { g_dispatcher().stopEvent(actionTaskEventPush); @@ -3371,7 +3392,7 @@ BlockType_t Player::blockHit(const std::shared_ptr &attacker, const Co } } - // + // Absorb Percent const ItemType &it = Item::items[item->getID()]; if (it.abilities) { int totalAbsorbPercent = 0; @@ -3387,7 +3408,7 @@ BlockType_t Player::blockHit(const std::shared_ptr &attacker, const Co } } - if (totalAbsorbPercent > 0) { + if (totalAbsorbPercent != 0) { damage -= std::round(damage * (totalAbsorbPercent / 100.0)); const auto charges = item->getAttribute(ItemAttribute_t::CHARGES); @@ -9938,9 +9959,7 @@ void Player::onFollowCreatureDisappear(bool isLogout) { } } -// container -// container - +// Container void Player::onAddContainerItem(const std::shared_ptr &item) { checkTradeState(item); } diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 1b52607f2..371048fbe 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -945,6 +945,13 @@ class Player final : public Creature, public Cylinder, public Bankable { void setNextPotionAction(int64_t time); bool canDoPotionAction() const; + void setLoginProtection(int64_t time); + bool isLoginProtected() const; + void resetLoginProtection(); + + void setProtection(bool status); + bool isProtected(); + void cancelPush(); void setModuleDelay(uint8_t byteortype, int16_t delay); @@ -1421,6 +1428,7 @@ class Player final : public Creature, public Cylinder, public Bankable { int64_t nextPotionAction = 0; int64_t lastQuickLootNotification = 0; int64_t lastWalking = 0; + int64_t loginProtectionTime = 0; uint64_t asyncOngoingTasks = 0; std::vector unjustifiedKills; @@ -1560,6 +1568,8 @@ class Player final : public Creature, public Cylinder, public Bankable { bool moved = false; bool m_isDead = false; bool imbuementTrackerWindowOpen = false; + bool shouldForceLogout = true; + bool connProtected = false; // Hazard system int64_t lastHazardSystemCriticalHit = 0; diff --git a/src/database/database.cpp b/src/database/database.cpp index fcb737170..a6e732694 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 69c47d324..3566425a5 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); diff --git a/src/game/game.cpp b/src/game/game.cpp index a4a53738b..80aea6c8b 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(); } @@ -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)"); @@ -954,11 +957,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 +974,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 +999,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 +3191,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; @@ -3371,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. @@ -3468,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) { @@ -3476,6 +3505,7 @@ void Game::playerMove(uint32_t playerId, Direction direction) { return; } + player->resetLoginProtection(); player->resetIdleTime(); player->setNextWalkActionTask(nullptr); player->cancelPush(); @@ -3489,6 +3519,7 @@ void Game::forcePlayerMove(uint32_t playerId, Direction direction) { return; } + player->resetLoginProtection(); player->resetIdleTime(); player->setNextWalkActionTask(nullptr); player->cancelPush(); @@ -3664,6 +3695,7 @@ void Game::playerAutoWalk(uint32_t playerId, const std::vector &listD return; } + player->resetLoginProtection(); player->resetIdleTime(); player->setNextWalkTask(nullptr); player->startAutoWalk(listDir, false); @@ -3680,6 +3712,7 @@ void Game::forcePlayerAutoWalk(uint32_t playerId, const std::vector & player->sendCancelTarget(); player->setFollowCreature(nullptr); + player->resetLoginProtection(); player->resetIdleTime(); player->setNextWalkTask(nullptr); @@ -3813,6 +3846,7 @@ void Game::playerUseItemEx(uint32_t playerId, const Position &fromPos, uint8_t f return; } + player->resetLoginProtection(); player->resetIdleTime(); if (it.isRune() || it.type == ITEM_TYPE_POTION) { player->setNextPotionActionTask(nullptr); @@ -3934,6 +3968,7 @@ void Game::playerUseItem(uint32_t playerId, const Position &pos, uint8_t stackPo return; } + player->resetLoginProtection(); player->resetIdleTime(); player->setNextActionTask(nullptr); @@ -4098,6 +4133,7 @@ void Game::playerUseWithCreature(uint32_t playerId, const Position &fromPos, uin return; } + player->resetLoginProtection(); player->resetIdleTime(); if (it.isRune() || it.type == ITEM_TYPE_POTION) { player->setNextPotionActionTask(nullptr); @@ -5882,6 +5918,7 @@ void Game::playerSetAttackedCreature(uint32_t playerId, uint32_t creatureId) { } if (player->getAttackedCreature() && creatureId == 0) { + player->resetLoginProtection(); player->setAttackedCreature(nullptr); player->sendCancelTarget(); return; @@ -6056,6 +6093,7 @@ void Game::playerTurn(uint32_t playerId, Direction dir) { return; } + player->resetLoginProtection(); player->resetIdleTime(); internalCreatureTurn(player, dir); } @@ -6179,6 +6217,7 @@ void Game::playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, c return; } + player->resetLoginProtection(); player->resetIdleTime(); if (playerSaySpell(player, type, text)) { @@ -8504,39 +8543,66 @@ 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)); +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); + + if (categoryName.empty()) { + g_logger().error("Category name cannot be empty."); + return ""; + } - 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`"; + std::string query = fmt::format( + "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 << ") `T` WHERE `rn` > " << startPage << " AND `rn` <= " << endPage; - return query.str(); -} + 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) { - std::ostringstream query; - std::string entriesStr = std::to_string(entriesPerPage); + if (playerGUID != 0) { + query += fmt::format(", @ourRow := IF(`id` = {}, @row - 1, @ourRow) AS `rw`", playerGUID); + } - 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`"; + 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` < {} ORDER BY `{}` DESC) `t`", + categoryName, static_cast(GROUP_TYPE_GAMEMASTER), categoryName + ); 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 += ") `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; } std::string Game::generateVocationConditionHighscore(uint32_t vocation) { @@ -8624,7 +8690,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; @@ -8642,7 +8708,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; @@ -8997,9 +9063,10 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite return; } - uint64_t calcFee = (price / 100) * amount; - uint64_t minFee = std::min(100000, calcFee); - uint64_t fee = std::max(20, minFee); + uint64_t totalPrice = price * amount; + uint64_t totalFee = totalPrice * 0.02; + uint64_t maxFee = std::min(1000000, totalFee); + uint64_t fee = std::max(20, totalFee); if (type == MARKETACTION_SELL) { if (fee > (player->getBankBalance() + player->getMoney())) { @@ -9930,19 +9997,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 { @@ -10151,7 +10271,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; @@ -10324,7 +10444,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; @@ -10540,7 +10660,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 b16d1787d..9a4b59f8a 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* @@ -949,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); diff --git a/src/game/zones/zone.hpp b/src/game/zones/zone.hpp index a3af961bd..e13652e8b 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; diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index e724d0ced..21d251c6d 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 3e8a05524..351f80177 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -21,45 +21,45 @@ 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()); + 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; - - 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; + 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 { + 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,54 @@ 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` = {}, `raceid` = '{}', ", + today, database.escapeString(bossName), bossId + ); + + if (const auto bossType = getMonsterTypeByBossRaceId(bossId); bossType) { + query += fmt::format( + "`looktypeEx` = {}, `looktype` = {}, `lookfeet` = {}, `looklegs` = {}, " + "`lookhead` = {}, `lookbody` = {}, `lookaddons` = {}, `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.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())) { + query += fmt::format("`raceid` = '{}'", bossId); + + if (!database.executeQuery(query)) { g_logger().error("[{}] Failed to detect boosted boss database. (CODE 03)", __FUNCTION__); return; } - query.str(std::string()); - query << "UPDATE `player_bosstiary` SET `bossIdSlotOne` = 0 WHERE `bossIdSlotOne` = " << bossId; - if (!database.executeQuery(query.str())) { + 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.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 (!database.executeQuery(query)) { + g_logger().error("[{}] Failed to reset players selected boss slot 2. (CODE 03)", __FUNCTION__); + return; } setBossBoostedName(bossName); diff --git a/src/lua/creature/movement.cpp b/src/lua/creature/movement.cpp index 6804cdfa7..51daea8cf 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; } diff --git a/src/lua/functions/core/game/lua_enums.cpp b/src/lua/functions/core/game/lua_enums.cpp index 2136a3e59..8bff42580 100644 --- a/src/lua/functions/core/game/lua_enums.cpp +++ b/src/lua/functions/core/game/lua_enums.cpp @@ -772,14 +772,22 @@ void LuaEnums::initCreatureTypeEnums(lua_State* L) { } void LuaEnums::initClientOsEnums(lua_State* L) { + registerEnum(L, CLIENTOS_NONE); registerEnum(L, CLIENTOS_LINUX); registerEnum(L, CLIENTOS_WINDOWS); registerEnum(L, CLIENTOS_FLASH); + registerEnum(L, CLIENTOS_NEW_LINUX); registerEnum(L, CLIENTOS_NEW_WINDOWS); registerEnum(L, CLIENTOS_NEW_MAC); registerEnum(L, CLIENTOS_OTCLIENT_LINUX); registerEnum(L, CLIENTOS_OTCLIENT_WINDOWS); registerEnum(L, CLIENTOS_OTCLIENT_MAC); + registerEnum(L, CLIENTOS_OTCLIENTV8_LINUX); + registerEnum(L, CLIENTOS_OTCLIENTV8_WINDOWS); + registerEnum(L, CLIENTOS_OTCLIENTV8_MAC); + registerEnum(L, CLIENTOS_OTCLIENTV8_ANDROID); + registerEnum(L, CLIENTOS_OTCLIENTV8_IOS); + registerEnum(L, CLIENTOS_OTCLIENTV8_WEB); } void LuaEnums::initFightModeEnums(lua_State* L) { diff --git a/src/lua/functions/creatures/monster/monster_type_functions.cpp b/src/lua/functions/creatures/monster/monster_type_functions.cpp index 07ce44b62..8b2f4a5f3 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; } diff --git a/src/lua/functions/items/item_type_functions.cpp b/src/lua/functions/items/item_type_functions.cpp index 9c12f80e5..a1751ed4f 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); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 60e298474..9d385be58 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -614,6 +614,7 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS player->lastIP = player->getIP(); player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); + player->loginProtectionTime = OTSYS_TIME() + g_configManager().getNumber(LOGIN_PROTECTION_TIME); acceptPackets = true; } else { if (eventConnect != 0 || !g_configManager().getBoolean(REPLACE_KICK_ON_LOGIN)) { @@ -666,6 +667,12 @@ void ProtocolGame::connect(const std::string &playerName, OperatingSystem_t oper sendAddCreature(player, player->getPosition(), 0, true); player->lastIP = player->getIP(); player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); + if (player->isProtected()) { + player->setProtection(false); + player->resetLoginProtection(); + } else { + player->setLoginProtection(g_configManager().getNumber(LOGIN_PROTECTION_TIME)); + } player->resetIdleTime(); acceptPackets = true; } diff --git a/src/utils/object_pool.hpp b/src/utils/object_pool.hpp new file mode 100644 index 000000000..c37d1d2ef --- /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;