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;