From 9adf04031bd86eb72376636f261b2feed2abb491 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Mon, 9 Dec 2024 14:48:54 -0300 Subject: [PATCH 01/24] perf: multithreading in updateTargetList in all events (#3074) Previously, the multithreading of updateTargetList only worked for walk events, now all requests to these methods will be executed async. --- src/creatures/monsters/monster.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 7276bea19d1..49472ec209f 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -602,7 +602,7 @@ bool Monster::removeTarget(const std::shared_ptr &creature) { } void Monster::updateTargetList() { - if (g_dispatcher().context().getGroup() == TaskGroup::Walk) { + if (!g_dispatcher().context().isAsync()) { setAsyncTaskFlag(UpdateTargetList, true); return; } From 3c98b4161cd9c25e57e61945fe6358d604242b27 Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 9 Dec 2024 15:14:36 -0300 Subject: [PATCH 02/24] refactor: split player death event handler into smaller functions (#3113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refactors the PlayerDeath event script to improve code readability and maintainability. The original function has been split into smaller, more focused functions, each handling a specific part of the player death processing logic. This includes identifying the killer, logging deaths in the database, sending messages to the guild channel, and checking guild wars. With this refactoring, the code is more modular and easier to understand, making future modifications and maintenance more manageable. The PlayerDeath event logic remains unchanged, ensuring the system continues to function as expected. Changes: •. Split the onDeath function into smaller local functions. •. Improved SQL query formatting using string.format for clarity and to avoid repetitive code. •. Renamed local variables to follow the camelCase convention. --- .../scripts/creaturescripts/player_death.lua | 114 ----------- .../creaturescripts/others/player_death.lua | 146 -------------- .../creaturescripts_player_death.lua | 9 + ...ssing_login.lua => adventure_blessing.lua} | 0 data/scripts/creaturescripts/player/death.lua | 183 ++++++++++++++++++ 5 files changed, 192 insertions(+), 260 deletions(-) delete mode 100644 data-canary/scripts/creaturescripts/player_death.lua delete mode 100644 data-otservbr-global/scripts/creaturescripts/others/player_death.lua create mode 100644 data-otservbr-global/scripts/quests/svargrond_arena/creaturescripts_player_death.lua rename data/scripts/creaturescripts/player/{adventure_blessing_login.lua => adventure_blessing.lua} (100%) create mode 100644 data/scripts/creaturescripts/player/death.lua diff --git a/data-canary/scripts/creaturescripts/player_death.lua b/data-canary/scripts/creaturescripts/player_death.lua deleted file mode 100644 index c30e2e98d99..00000000000 --- a/data-canary/scripts/creaturescripts/player_death.lua +++ /dev/null @@ -1,114 +0,0 @@ -local playerDeath = CreatureEvent("PlayerDeath") - -local deathListEnabled = true -local maxDeathRecords = 5 - -function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) - if not deathListEnabled then - return - end - - local byPlayer = 0 - local killerName - if killer then - if killer:isPlayer() then - byPlayer = 1 - else - local master = killer:getMaster() - if master and master ~= killer and master:isPlayer() then - killer = master - byPlayer = 1 - end - end - killerName = killer:getName() - else - killerName = "field item" - end - - local byPlayerMostDamage = 0 - local mostDamageKillerName - if mostDamageKiller then - if mostDamageKiller:isPlayer() then - byPlayerMostDamage = 1 - else - local master = mostDamageKiller:getMaster() - if master and master ~= mostDamageKiller and master:isPlayer() then - mostDamageKiller = master - byPlayerMostDamage = 1 - end - end - mostDamageName = mostDamageKiller:getName() - else - mostDamageName = "field item" - end - - player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE) - - if mostDamageKiller and mostDamageKiller:isPlayer() and killer ~= mostDamageKiller then - mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - end - - local playerGuid = player:getGuid() - db.query( - "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (" - .. playerGuid - .. ", " - .. os.time() - .. ", " - .. player:getLevel() - .. ", " - .. db.escapeString(killerName) - .. ", " - .. byPlayer - .. ", " - .. db.escapeString(mostDamageName) - .. ", " - .. byPlayerMostDamage - .. ", " - .. (unjustified and 1 or 0) - .. ", " - .. (mostDamageUnjustified and 1 or 0) - .. ")" - ) - local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) - - local deathRecords = 0 - local tmpResultId = resultId - while tmpResultId ~= false do - tmpResultId = Result.next(resultId) - deathRecords = deathRecords + 1 - end - - if resultId ~= false then - Result.free(resultId) - end - - local limit = deathRecords - maxDeathRecords - if limit > 0 then - db.asyncQuery("DELETE FROM `player_deaths` WHERE `player_id` = " .. playerGuid .. " ORDER BY `time` LIMIT " .. limit) - end - - if byPlayer == 1 then - killer:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - local targetGuild = player:getGuild() - targetGuild = targetGuild and targetGuild:getId() or 0 - if targetGuild ~= 0 then - local killerGuild = killer:getGuild() - killerGuild = killerGuild and killerGuild:getId() or 0 - if killerGuild ~= 0 and targetGuild ~= killerGuild and isInWar(player:getId(), killer:getId()) then - local warId = false - resultId = db.storeQuery("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = " .. killerGuild .. " AND `guild2` = " .. targetGuild .. ") OR (`guild1` = " .. targetGuild .. " AND `guild2` = " .. killerGuild .. "))") - if resultId ~= false then - warId = Result.getNumber(resultId, "id") - Result.free(resultId) - end - - if warId ~= false then - db.asyncQuery("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES (" .. db.escapeString(killerName) .. ", " .. db.escapeString(player:getName()) .. ", " .. killerGuild .. ", " .. targetGuild .. ", " .. os.time() .. ", " .. warId .. ")") - end - end - end - end -end - -playerDeath:register() diff --git a/data-otservbr-global/scripts/creaturescripts/others/player_death.lua b/data-otservbr-global/scripts/creaturescripts/others/player_death.lua deleted file mode 100644 index f3a84454cfd..00000000000 --- a/data-otservbr-global/scripts/creaturescripts/others/player_death.lua +++ /dev/null @@ -1,146 +0,0 @@ -local deathListEnabled = true - -local playerDeath = CreatureEvent("PlayerDeath") -function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) - if player:getStorageValue(Storage.Quest.U8_0.BarbarianArena.PitDoor) > 0 then - player:setStorageValue(Storage.Quest.U8_0.BarbarianArena.PitDoor, 0) - end - - if not deathListEnabled then - return - end - - local byPlayer = 0 - local killerName - if killer ~= nil then - if killer:isPlayer() then - byPlayer = 1 - else - local master = killer:getMaster() - if master and master ~= killer and master:isPlayer() then - killer = master - byPlayer = 1 - end - end - killerName = killer:isMonster() and killer:getType():getNameDescription() or killer:getName() - else - killerName = "field item" - end - - local byPlayerMostDamage = 0 - local mostDamageKillerName - if mostDamageKiller ~= nil then - if mostDamageKiller:isPlayer() then - byPlayerMostDamage = 1 - else - local master = mostDamageKiller:getMaster() - if master and master ~= mostDamageKiller and master:isPlayer() then - mostDamageKiller = master - byPlayerMostDamage = 1 - end - end - mostDamageName = mostDamageKiller:isMonster() and mostDamageKiller:getType():getNameDescription() or mostDamageKiller:getName() - else - mostDamageName = "field item" - end - - player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE) - - if mostDamageKiller and mostDamageKiller:isPlayer() then - mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - end - - local playerGuid = player:getGuid() - db.query( - "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (" - .. playerGuid - .. ", " - .. os.time() - .. ", " - .. player:getLevel() - .. ", " - .. db.escapeString(killerName) - .. ", " - .. byPlayer - .. ", " - .. db.escapeString(mostDamageName) - .. ", " - .. byPlayerMostDamage - .. ", " - .. (unjustified and 1 or 0) - .. ", " - .. (mostDamageUnjustified and 1 or 0) - .. ")" - ) - local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) - -- Start Webhook Player Death - Webhook.sendMessage(":skull_crossbones: " .. player:getMarkdownLink() .. " has died. Killed at level _" .. player:getLevel() .. "_ by **" .. killerName .. "**.", announcementChannels["player-kills"]) - -- End Webhook Player Death - - local deathRecords = 0 - local tmpResultId = resultId - while tmpResultId ~= false do - tmpResultId = Result.next(resultId) - deathRecords = deathRecords + 1 - end - - if resultId ~= false then - Result.free(resultId) - end - - if byPlayer == 1 then - killer:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - local targetGuild = player:getGuild() - local targetGuildId = targetGuild and targetGuild:getId() or 0 - if targetGuildId ~= 0 then - local killerGuild = killer:getGuild() - local killerGuildId = killerGuild and killerGuild:getId() or 0 - if killerGuildId ~= 0 and targetGuildId ~= killerGuildId and isInWar(player:getId(), killer:getId()) then - local warId = false - resultId = db.storeQuery("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND \z - ((`guild1` = " .. killerGuildId .. " AND `guild2` = " .. targetGuildId .. ") OR \z - (`guild1` = " .. targetGuildId .. " AND `guild2` = " .. killerGuildId .. "))") - if resultId then - warId = Result.getNumber(resultId, "id") - Result.free(resultId) - end - - if warId then - local playerName = player:getName() - db.asyncQuery("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) \z - VALUES (" .. db.escapeString(killerName) .. ", " .. db.escapeString(playerName) .. ", " .. killerGuildId .. ", \z - " .. targetGuildId .. ", " .. os.time() .. ", " .. warId .. ")") - - resultId = db.storeQuery("SELECT `guild_wars`.`id`, `guild_wars`.`frags_limit`, (SELECT COUNT(1) FROM `guildwar_kills` \z - WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild1`) AS guild1_kills, \z - (SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild2`) AS guild2_kills \z - FROM `guild_wars` WHERE (`guild1` = " .. killerGuildId .. " OR `guild2` = " .. killerGuildId .. ") AND `status` = 1 AND `id` = " .. warId) - - if resultId then - local guild1_kills = Result.getNumber(resultId, "guild1_kills") - local guild2_kills = Result.getNumber(resultId, "guild2_kills") - local frags_limit = Result.getNumber(resultId, "frags_limit") - Result.free(resultId) - - local members = killerGuild:getMembersOnline() - for i = 1, #members do - members[i]:sendChannelMessage(members[i], string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1_kills, guild2_kills, killerGuild:getName(), frags_limit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) - end - - local enemyMembers = targetGuild:getMembersOnline() - for i = 1, #enemyMembers do - enemyMembers[i]:sendChannelMessage(enemyMembers[i], string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1_kills, guild2_kills, killerGuild:getName(), frags_limit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) - end - - if guild1_kills >= frags_limit or guild2_kills >= frags_limit then - db.query("UPDATE `guild_wars` SET `status` = 4, `ended` = " .. os.time() .. " WHERE `status` = 1 AND `id` = " .. warId) - Game.broadcastMessage(string.format("%s has just won the war against %s.", killerGuild:getName(), targetGuild:getName())) - end - end - end - end - end - end -end - -playerDeath:register() diff --git a/data-otservbr-global/scripts/quests/svargrond_arena/creaturescripts_player_death.lua b/data-otservbr-global/scripts/quests/svargrond_arena/creaturescripts_player_death.lua new file mode 100644 index 00000000000..e1ba265090a --- /dev/null +++ b/data-otservbr-global/scripts/quests/svargrond_arena/creaturescripts_player_death.lua @@ -0,0 +1,9 @@ +local svargrondArenaPlayerDeath = CreatureEvent("SvargrondArenaPlayerDeath") + +function svargrondArenaPlayerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) + if player:getStorageValue(Storage.Quest.U8_0.BarbarianArena.PitDoor) > 0 then + player:setStorageValue(Storage.Quest.U8_0.BarbarianArena.PitDoor, 0) + end +end + +svargrondArenaPlayerDeath:register() diff --git a/data/scripts/creaturescripts/player/adventure_blessing_login.lua b/data/scripts/creaturescripts/player/adventure_blessing.lua similarity index 100% rename from data/scripts/creaturescripts/player/adventure_blessing_login.lua rename to data/scripts/creaturescripts/player/adventure_blessing.lua diff --git a/data/scripts/creaturescripts/player/death.lua b/data/scripts/creaturescripts/player/death.lua new file mode 100644 index 00000000000..42d143781d4 --- /dev/null +++ b/data/scripts/creaturescripts/player/death.lua @@ -0,0 +1,183 @@ +local deathListEnabled = true + +local function getKillerInfo(killer) + local byPlayer = 0 + local killerName + + if killer then + if killer:isPlayer() then + byPlayer = 1 + else + local master = killer:getMaster() + if master and master ~= killer and master:isPlayer() then + killer = master + byPlayer = 1 + end + end + + killerName = killer:isMonster() and killer:getType():getNameDescription() or killer:getName() + else + killerName = "field item" + end + + return killerName, byPlayer +end + +local function getMostDamageInfo(mostDamageKiller) + local byPlayerMostDamage = 0 + local mostDamageKillerName + + if mostDamageKiller then + if mostDamageKiller:isPlayer() then + byPlayerMostDamage = 1 + else + local master = mostDamageKiller:getMaster() + if master and master ~= mostDamageKiller and master:isPlayer() then + mostDamageKiller = master + byPlayerMostDamage = 1 + end + end + + mostDamageKillerName = mostDamageKiller:isMonster() and mostDamageKiller:getType():getNameDescription() or mostDamageKiller:getName() + else + mostDamageKillerName = "field item" + end + + return mostDamageKillerName, byPlayerMostDamage +end + +local function saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDamageName, byPlayerMostDamage, unjustified, mostDamageUnjustified) + local query = string.format( + "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) " .. "VALUES (%d, %d, %d, '%s', %d, '%s', %d, %d, %d)", + playerGuid, + os.time(), + player:getLevel(), + db.escapeString(killerName), + byPlayer, + db.escapeString(mostDamageName), + byPlayerMostDamage, + unjustified and 1 or 0, + mostDamageUnjustified and 1 or 0 + ) + db.query(query) +end + +local function handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) + local deathRecords = getDeathRecords(player:getGuid()) + + if deathRecords > 0 then + local targetGuildId = player:getGuild() and player:getGuild():getId() or 0 + local killerGuildId = killer:getGuild() and killer:getGuild():getId() or 0 + + if targetGuildId ~= 0 and killerGuildId ~= 0 and targetGuildId ~= killerGuildId then + local warId = checkForGuildWar(targetGuildId, killerGuildId) + if warId then + recordGuildWarKill(killer, player, killerGuildId, targetGuildId, warId) + checkAndUpdateGuildWarScore(warId, targetGuildId, killerGuildId, player:getName(), killerName, mostDamageName) + end + end + end +end + +local function getDeathRecords(playerGuid) + local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) + local deathRecords = 0 + while resultId do + resultId = Result.next(resultId) + deathRecords = deathRecords + 1 + end + + if resultId then + Result.free(resultId) + end + + return deathRecords +end + +local function checkForGuildWar(targetGuildId, killerGuildId) + local resultId = db.storeQuery(string.format("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = %d AND `guild2` = %d) OR (`guild1` = %d AND `guild2` = %d))", killerGuildId, targetGuildId, targetGuildId, killerGuildId)) + + local warId = false + if resultId then + warId = Result.getNumber(resultId, "id") + Result.free(resultId) + end + + return warId +end + +local function recordGuildWarKill(killer, player, killerGuildId, targetGuildId, warId) + local playerName = player:getName() + db.asyncQuery(string.format("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES ('%s', '%s', %d, %d, %d, %d)", db.escapeString(killer:getName()), db.escapeString(playerName), killerGuildId, targetGuildId, os.time(), warId)) +end + +local function checkAndUpdateGuildWarScore(warId, targetGuildId, killerGuildId, playerName, killerName, mostDamageName) + local resultId = db.storeQuery( + string.format( + "SELECT `guild_wars`.`id`, `guild_wars`.`frags_limit`, " + .. "(SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild1`) AS guild1_kills, " + .. "(SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild2`) AS guild2_kills " + .. "FROM `guild_wars` WHERE (`guild1` = %d OR `guild2` = %d) AND `status` = 1 AND `id` = %d", + killerGuildId, + targetGuildId, + warId + ) + ) + + if resultId then + local guild1Kills = Result.getNumber(resultId, "guild1_kills") + local guild2Kills = Result.getNumber(resultId, "guild2_kills") + local fragsLimit = Result.getNumber(resultId, "frags_limit") + Result.free(resultId) + + local killerGuild = killer:getGuild() + local targetGuild = player:getGuild() + + updateGuildWarScore(killerGuild, targetGuild, playerName, killerName, guild1Kills, guild2Kills, fragsLimit) + endGuildWarIfLimitReached(guild1Kills, guild2Kills, fragsLimit, warId, killerGuild, targetGuild) + end +end + +local function updateGuildWarScore(killerGuild, targetGuild, playerName, killerName, guild1Kills, guild2Kills, fragsLimit) + local members = killerGuild:getMembersOnline() + for _, member in ipairs(members) do + member:sendChannelMessage(member, string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1Kills, guild2Kills, killerGuild:getName(), fragsLimit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) + end + + local enemyMembers = targetGuild:getMembersOnline() + for _, enemy in ipairs(enemyMembers) do + enemy:sendChannelMessage(enemy, string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1Kills, guild2Kills, killerGuild:getName(), fragsLimit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) + end +end + +local function endGuildWarIfLimitReached(guild1Kills, guild2Kills, fragsLimit, warId, killerGuild, targetGuild) + if guild1Kills >= fragsLimit or guild2Kills >= fragsLimit then + db.query(string.format("UPDATE `guild_wars` SET `status` = 4, `ended` = %d WHERE `status` = 1 AND `id` = %d", os.time(), warId)) + Game.broadcastMessage(string.format("%s has just won the war against %s.", killerGuild:getName(), targetGuild:getName())) + end +end + +local playerDeath = CreatureEvent("PlayerDeath") + +function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) + if not deathListEnabled then + return + end + + local killerName, byPlayer = getKillerInfo(killer) + local mostDamageName, byPlayerMostDamage = getMostDamageInfo(mostDamageKiller) + + player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE) + + if mostDamageKiller and mostDamageKiller:isPlayer() then + mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) + end + + local playerGuid = player:getGuid() + saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDamageName, byPlayerMostDamage, unjustified, mostDamageUnjustified) + + Webhook.sendMessage(":skull_crossbones: " .. player:getMarkdownLink() .. " has died. Killed at level _" .. player:getLevel() .. "_ by **" .. killerName .. "**.", announcementChannels["player-kills"]) + handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) +end + +playerDeath:register() From 74b8ed0a5ffc2e9447aaf7d9f4c29ddf7434d0d9 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Mon, 9 Dec 2024 15:52:42 -0300 Subject: [PATCH 03/24] perf: onRemoveCreature->onCreatureLeave async (#3152) the call to onCreatureLeave in Monster::onRemoveCreature will be called asynchronously. --- src/creatures/monsters/monster.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 49472ec209f..72816fd1a43 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -354,7 +354,9 @@ void Monster::onRemoveCreature(const std::shared_ptr &creature, bool i setIdle(true); } else { - onCreatureLeave(creature); + addAsyncTask([this, creature] { + onCreatureLeave(creature); + }); } } From fbd622861dc56572007b79acb138836cd03a7dde Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 9 Dec 2024 15:53:51 -0300 Subject: [PATCH 04/24] fix: resolve nil value errors in handleGuildWar function (#3172) I made corrections to the `death.lua` script to resolve nil value errors in the `handleGuildWar` function. I added checks to ensure that both the player and the killer are valid and that both belong to a guild before proceeding with the guild war logic. This helps prevent failures when one of the objects is undefined. Fixes from: https://github.com/opentibiabr/canary/commit/3c98b4161cd9c25e57e61945fe6358d604242b27 --- data/scripts/creaturescripts/player/death.lua | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/data/scripts/creaturescripts/player/death.lua b/data/scripts/creaturescripts/player/death.lua index 42d143781d4..4c0c9a8b2ca 100644 --- a/data/scripts/creaturescripts/player/death.lua +++ b/data/scripts/creaturescripts/player/death.lua @@ -48,7 +48,7 @@ end local function saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDamageName, byPlayerMostDamage, unjustified, mostDamageUnjustified) local query = string.format( - "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) " .. "VALUES (%d, %d, %d, '%s', %d, '%s', %d, %d, %d)", + "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) " .. "VALUES (%d, %d, %d, %s, %d, %s, %d, %d, %d)", playerGuid, os.time(), player:getLevel(), @@ -62,23 +62,6 @@ local function saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDam db.query(query) end -local function handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) - local deathRecords = getDeathRecords(player:getGuid()) - - if deathRecords > 0 then - local targetGuildId = player:getGuild() and player:getGuild():getId() or 0 - local killerGuildId = killer:getGuild() and killer:getGuild():getId() or 0 - - if targetGuildId ~= 0 and killerGuildId ~= 0 and targetGuildId ~= killerGuildId then - local warId = checkForGuildWar(targetGuildId, killerGuildId) - if warId then - recordGuildWarKill(killer, player, killerGuildId, targetGuildId, warId) - checkAndUpdateGuildWarScore(warId, targetGuildId, killerGuildId, player:getName(), killerName, mostDamageName) - end - end - end -end - local function getDeathRecords(playerGuid) local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) local deathRecords = 0 @@ -94,6 +77,27 @@ local function getDeathRecords(playerGuid) return deathRecords end +local function handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) + if not player or not killer or not killer:isPlayer() or not player:getGuild() or not killer:getGuild() then + return + end + + local playerGuildId = player:getGuild():getId() + local killerGuildId = killer:getGuild():getId() + + if playerGuildId == killerGuildId then + return + end + + if getDeathRecords(player:getGuid()) > 0 then + local warId = checkForGuildWar(playerGuildId, killerGuildId) + if warId then + recordGuildWarKill(killer, player, killerGuildId, playerGuildId, warId) + checkAndUpdateGuildWarScore(warId, playerGuildId, killerGuildId, player:getName(), killerName, mostDamageName) + end + end +end + local function checkForGuildWar(targetGuildId, killerGuildId) local resultId = db.storeQuery(string.format("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = %d AND `guild2` = %d) OR (`guild1` = %d AND `guild2` = %d))", killerGuildId, targetGuildId, targetGuildId, killerGuildId)) From 8891f3afc1fffb86879dc5060aefe101c3b16b37 Mon Sep 17 00:00:00 2001 From: kokekanon <114332266+kokekanon@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:27:17 -0300 Subject: [PATCH 05/24] fix: packet interpretation parseSetOutfit for otcv8/old protocol (#3162) Resolves #3155 --- src/server/network/protocol/protocolgame.cpp | 30 +++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 2f7ff9e2479..9c09f5ead38 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -1689,7 +1689,7 @@ void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { g_logger().debug("Bool isMounted: {}", isMounted); } - uint8_t isMountRandomized = msg.getByte(); + uint8_t isMountRandomized = !oldProtocol ? msg.getByte() : 0; g_game().playerChangeOutfit(player->getID(), newOutfit, isMountRandomized); } else if (outfitType == 1) { // This value probably has something to do with try outfit variable inside outfit window dialog @@ -3247,12 +3247,6 @@ void ProtocolGame::sendCreatureOutfit(const std::shared_ptr &creature, msg.add(creature->getID()); AddOutfit(msg, newOutfit); - if (!oldProtocol && newOutfit.lookMount != 0) { - msg.addByte(newOutfit.lookMountHead); - msg.addByte(newOutfit.lookMountBody); - msg.addByte(newOutfit.lookMountLegs); - msg.addByte(newOutfit.lookMountFeet); - } writeToOutputBuffer(msg); } @@ -7184,10 +7178,12 @@ void ProtocolGame::sendOutfitWindow() { return; } - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountHead); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountBody); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountLegs); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountFeet); + if (currentOutfit.lookMount == 0) { + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountHead); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountBody); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountLegs); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountFeet); + } msg.add(currentOutfit.lookFamiliarsType); auto startOutfits = msg.getBufferPosition(); @@ -7750,12 +7746,6 @@ void ProtocolGame::AddCreature(NetworkMessage &msg, const std::shared_ptrisInGhostMode() && !creature->isInvisible()) { const Outfit_t &outfit = creature->getCurrentOutfit(); AddOutfit(msg, outfit); - if (!oldProtocol && outfit.lookMount != 0) { - msg.addByte(outfit.lookMountHead); - msg.addByte(outfit.lookMountBody); - msg.addByte(outfit.lookMountLegs); - msg.addByte(outfit.lookMountFeet); - } } else { static Outfit_t outfit; AddOutfit(msg, outfit); @@ -7945,6 +7935,12 @@ void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, bool a if (addMount) { msg.add(outfit.lookMount); + if (!oldProtocol && outfit.lookMount != 0) { + msg.addByte(outfit.lookMountHead); + msg.addByte(outfit.lookMountBody); + msg.addByte(outfit.lookMountLegs); + msg.addByte(outfit.lookMountFeet); + } } } From a81297ae5e9e487f1a437612075de23d47ad2820 Mon Sep 17 00:00:00 2001 From: odisk777 <65802862+odisk777@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:28:23 -0500 Subject: [PATCH 06/24] Compatibility with Basic CPUs (#3146) Some servers or vps now come with modifications for basic cpu such as: QEMU Virtual CPU version 2.5+, this function makes it possible to run canary --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d143fec490..d7ba93bafb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,11 @@ endif() # ***************************************************************************** list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) +# Configure build options for compatibility with commodity CPUs +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") + + # ***************************************************************************** # Include cmake tools # ***************************************************************************** From dec87cb0c6077032f9c95615e8381c6e61e73545 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Mon, 9 Dec 2024 16:29:12 -0300 Subject: [PATCH 07/24] Improve: creature actions (#3084) Some creature actions, such as checkCreatureAttack, checkCreatureWalk, updateCreatureWalk, were processed based on the creature ID, as canary works with smart pointer, we no longer have problems working with the object reference, before it used the ID to maintain security, because at the time of execution, the object could no longer exist. --- src/creatures/creature.cpp | 50 ++++++++++++++++++++++++------ src/creatures/creature.hpp | 6 ++++ src/creatures/monsters/monster.cpp | 2 +- src/creatures/players/player.cpp | 11 ++++--- src/game/game.cpp | 34 ++------------------ src/game/game.hpp | 4 --- src/game/scheduling/task.hpp | 3 +- 7 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 32f3c598b81..586f5b8c1b2 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -140,6 +140,20 @@ void Creature::onThink(uint32_t interval) { onThink(); } +void Creature::checkCreatureAttack(bool now) { + if (now) { + if (isAlive()) { + onAttacking(0); + } + return; + } + + g_dispatcher().addEvent([self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->checkCreatureAttack(true); + } }, "Creature::checkCreatureAttack"); +} + void Creature::onAttacking(uint32_t interval) { const auto &attackedCreature = getAttackedCreature(); if (!attackedCreature) { @@ -162,7 +176,7 @@ void Creature::onIdleStatus() { } void Creature::onCreatureWalk() { - if (checkingWalkCreature) { + if (checkingWalkCreature || isRemoved() || isDead()) { return; } @@ -170,7 +184,11 @@ void Creature::onCreatureWalk() { metrics::method_latency measure(__METRICS_METHOD_NAME__); - g_dispatcher().addWalkEvent([self = getCreature(), this] { + g_dispatcher().addWalkEvent([self = std::weak_ptr(getCreature()), this] { + if (!self.lock()) { + return; + } + checkingWalkCreature = false; if (isRemoved()) { return; @@ -269,12 +287,16 @@ void Creature::addEventWalk(bool firstStep) { safeCall([this, ticks]() { // Take first step right away, but still queue the next if (ticks == 1) { - g_game().checkCreatureWalk(getID()); + onCreatureWalk(); } eventWalk = g_dispatcher().scheduleEvent( - static_cast(ticks), - [creatureId = getID()] { g_game().checkCreatureWalk(creatureId); }, "Game::checkCreatureWalk" + static_cast(ticks), [self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->onCreatureWalk(); + } + }, + "Game::checkCreatureWalk" ); }); } @@ -421,7 +443,7 @@ void Creature::onCreatureMove(const std::shared_ptr &creature, const s if (followCreature && (creature.get() == this || creature == followCreature)) { if (hasFollowPath) { isUpdatingPath = true; - g_game().updateCreatureWalk(getID()); // internally uses addEventWalk. + updateCreatureWalk(); } if (newPos.z != oldPos.z || !canSee(followCreature->getPosition())) { @@ -436,7 +458,7 @@ void Creature::onCreatureMove(const std::shared_ptr &creature, const s } else { if (hasExtraSwing()) { // our target is moving lets see if we can get in hit - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, "Game::checkCreatureAttack"); + checkCreatureAttack(); } if (newTile->getZoneType() != oldTile->getZoneType()) { @@ -701,7 +723,13 @@ void Creature::changeHealth(int32_t healthChange, bool sendHealthChange /* = tru g_game().addCreatureHealth(static_self_cast()); } if (health <= 0) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().executeDeath(creatureId); }, "Game::executeDeath"); + g_dispatcher().addEvent([self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + if (!creature->isRemoved()) { + g_game().afterCreatureZoneChange(creature, creature->getZones(), {}); + creature->onDeath(); + } + } }, "Game::executeDeath"); } } @@ -874,6 +902,10 @@ void Creature::getPathSearchParams(const std::shared_ptr &, FindPathPa } void Creature::goToFollowCreature_async(std::function &&onComplete) { + if (isDead()) { + return; + } + if (!hasAsyncTaskFlag(Pathfinder) && onComplete) { g_dispatcher().addEvent(std::move(onComplete), "goToFollowCreature_async"); } @@ -1781,7 +1813,7 @@ void Creature::sendAsyncTasks() { setAsyncTaskFlag(AsyncTaskRunning, true); g_dispatcher().asyncEvent([self = std::weak_ptr(getCreature())] { if (const auto &creature = self.lock()) { - if (!creature->isRemoved()) { + if (!creature->isRemoved() && creature->isAlive()) { for (const auto &task : creature->asyncTasks) { task(); } diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index fcea0a6b0f2..18a4bd817ab 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -312,6 +312,9 @@ class Creature : virtual public Thing, public SharedObject { void addEventWalk(bool firstStep = false); void stopEventWalk(); + void updateCreatureWalk() { + goToFollowCreature_async(); + } void goToFollowCreature_async(std::function &&onComplete = nullptr); virtual void goToFollowCreature(); @@ -482,6 +485,9 @@ class Creature : virtual public Thing, public SharedObject { void setCreatureLight(LightInfo lightInfo); virtual void onThink(uint32_t interval); + + void checkCreatureAttack(bool now = false); + void onAttacking(uint32_t interval); virtual void onCreatureWalk(); virtual bool getNextStep(Direction &dir, uint32_t &flags); diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 72816fd1a43..4189ffc6c29 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -954,7 +954,7 @@ bool Monster::selectTarget(const std::shared_ptr &creature) { if (isHostile() || isSummon()) { if (setAttackedCreature(creature)) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, __FUNCTION__); + checkCreatureAttack(); } } return setFollowCreature(creature); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 4335db2bd53..88223534497 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -3430,9 +3430,10 @@ void Player::doAttacking(uint32_t interval) { } const auto &task = createPlayerTask( - std::max(SCHEDULER_MINTICKS, delay), - [playerId = getID()] { g_game().checkCreatureAttack(playerId); }, - __FUNCTION__ + std::max(SCHEDULER_MINTICKS, delay), [self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->checkCreatureAttack(true); + } }, __FUNCTION__ ); if (!classicSpeed) { @@ -5296,7 +5297,7 @@ bool Player::setAttackedCreature(const std::shared_ptr &creature) { } if (creature) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, __FUNCTION__); + checkCreatureAttack(); } return true; } @@ -9835,7 +9836,7 @@ void Player::onCreatureMove(const std::shared_ptr &creature, const std const auto &followCreature = getFollowCreature(); if (hasFollowPath && (creature == followCreature || (creature.get() == this && followCreature))) { isUpdatingPath = false; - g_game().updateCreatureWalk(getID()); // internally uses addEventWalk. + updateCreatureWalk(); } if (creature != getPlayer()) { diff --git a/src/game/game.cpp b/src/game/game.cpp index 7a492348649..4dc0b9e3e20 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1255,15 +1255,6 @@ bool Game::removeCreature(const std::shared_ptr &creature, bool isLogo return true; } -void Game::executeDeath(uint32_t creatureId) { - metrics::method_latency measure(__METRICS_METHOD_NAME__); - std::shared_ptr creature = getCreatureByID(creatureId); - if (creature && !creature->isRemoved()) { - afterCreatureZoneChange(creature, creature->getZones(), {}); - creature->onDeath(); - } -} - void Game::playerTeleport(uint32_t playerId, const Position &newPosition) { metrics::method_latency measure(__METRICS_METHOD_NAME__); const auto &player = getPlayerByID(playerId); @@ -5912,7 +5903,7 @@ void Game::playerSetAttackedCreature(uint32_t playerId, uint32_t creatureId) { } player->setAttackedCreature(attackCreature); - updateCreatureWalk(player->getID()); // internally uses addEventWalk. + player->updateCreatureWalk(); } void Game::playerFollowCreature(uint32_t playerId, uint32_t creatureId) { @@ -5922,7 +5913,7 @@ void Game::playerFollowCreature(uint32_t playerId, uint32_t creatureId) { } player->setAttackedCreature(nullptr); - updateCreatureWalk(player->getID()); // internally uses addEventWalk. + player->updateCreatureWalk(); player->setFollowCreature(getCreatureByID(creatureId)); } @@ -6433,27 +6424,6 @@ bool Game::internalCreatureSay(const std::shared_ptr &creature, SpeakC return true; } -void Game::checkCreatureWalk(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->onCreatureWalk(); - } -} - -void Game::updateCreatureWalk(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->goToFollowCreature_async(); - } -} - -void Game::checkCreatureAttack(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->onAttacking(0); - } -} - void Game::addCreatureCheck(const std::shared_ptr &creature) { if (creature->isRemoved()) { return; diff --git a/src/game/game.hpp b/src/game/game.hpp index b1afd810100..ff2e127fd25 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -173,7 +173,6 @@ class Game { bool placeCreature(const std::shared_ptr &creature, const Position &pos, bool extendedPos = false, bool force = false); bool removeCreature(const std::shared_ptr &creature, bool isLogout = true); - void executeDeath(uint32_t creatureId); void addCreatureCheck(const std::shared_ptr &creature); static void removeCreatureCheck(const std::shared_ptr &creature); @@ -437,9 +436,6 @@ class Game { void setGameState(GameState_t newState); // Events - void checkCreatureWalk(uint32_t creatureId); - void updateCreatureWalk(uint32_t creatureId); - void checkCreatureAttack(uint32_t creatureId); void checkCreatures(); void checkLight(); diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index 6cd9eef342d..c4de64059bb 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -66,14 +66,13 @@ class Task { const static std::unordered_set tasksContext = { "Decay::checkDecay", "Dispatcher::asyncEvent", - "Game::checkCreatureAttack", + "Creature::checkCreatureAttack", "Game::checkCreatureWalk", "Game::checkCreatures", "Game::checkImbuements", "Game::checkLight", "Game::createFiendishMonsters", "Game::createInfluencedMonsters", - "Game::updateCreatureWalk", "Game::updateForgeableMonsters", "Game::addCreatureCheck", "GlobalEvents::think", From e3259655c855c58279cdef2dc50f95bdf9375fc5 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Mon, 9 Dec 2024 16:30:04 -0300 Subject: [PATCH 08/24] perf: onThink multithreading (#3075) --- src/creatures/creature.hpp | 3 ++- src/creatures/monsters/monster.cpp | 40 ++++++++++++++---------------- src/creatures/monsters/monster.hpp | 2 ++ src/game/game.cpp | 14 +++++++++-- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 18a4bd817ab..35f39292609 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -700,7 +700,8 @@ class Creature : virtual public Thing, public SharedObject { AsyncTaskRunning = 1 << 0, UpdateTargetList = 1 << 1, UpdateIdleStatus = 1 << 2, - Pathfinder = 1 << 3 + Pathfinder = 1 << 3, + OnThink = 1 << 4, }; virtual bool isDead() const { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 4189ffc6c29..d30c2a33471 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -423,27 +423,13 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st if (const auto &nextTile = g_game().map.getTile(checkPosition)) { const auto &topCreature = nextTile->getTopCreature(); if (followCreature != topCreature && isOpponent(topCreature)) { - g_dispatcher().addEvent([selfWeak = std::weak_ptr(getMonster()), topCreatureWeak = std::weak_ptr(topCreature)] { - const auto &self = selfWeak.lock(); - const auto &topCreature = topCreatureWeak.lock(); - if (self && topCreature) { - self->selectTarget(topCreature); - } - }, - "Monster::onCreatureMove"); + selectTarget(topCreature); } } } } else if (isOpponent(creature)) { // we have no target lets try pick this one - g_dispatcher().addEvent([selfWeak = std::weak_ptr(getMonster()), creatureWeak = std::weak_ptr(creature)] { - const auto &self = selfWeak.lock(); - const auto &creaturePtr = creatureWeak.lock(); - if (self && creaturePtr) { - self->selectTarget(creaturePtr); - } - }, - "Monster::onCreatureMove"); + selectTarget(creature); } } }; @@ -978,7 +964,7 @@ void Monster::setIdle(bool idle) { } void Monster::updateIdleStatus() { - if (g_dispatcher().context().getGroup() == TaskGroup::Walk) { + if (!g_dispatcher().context().isAsync()) { setAsyncTaskFlag(UpdateIdleStatus, true); return; } @@ -1073,8 +1059,11 @@ void Monster::onThink(uint32_t interval) { } updateIdleStatus(); + setAsyncTaskFlag(OnThink, true); +} - if (isIdle) { +void Monster::onThink_async() { + if (isIdle) { // updateIdleStatus(); is executed before this method return; } @@ -1109,10 +1098,13 @@ void Monster::onThink(uint32_t interval) { } } - onThinkTarget(interval); - onThinkYell(interval); - onThinkDefense(interval); - onThinkSound(interval); + onThinkTarget(EVENT_CREATURE_THINK_INTERVAL); + + safeCall([this] { + onThinkYell(EVENT_CREATURE_THINK_INTERVAL); + onThinkDefense(EVENT_CREATURE_THINK_INTERVAL); + onThinkSound(EVENT_CREATURE_THINK_INTERVAL); + }); } void Monster::doAttacking(uint32_t interval) { @@ -2661,4 +2653,8 @@ void Monster::onExecuteAsyncTasks() { if (hasAsyncTaskFlag(UpdateIdleStatus)) { updateIdleStatus(); } + + if (hasAsyncTaskFlag(OnThink)) { + onThink_async(); + } } diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index b8a6087acc1..145b973fdc9 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -225,6 +225,8 @@ class Monster final : public Creature { void onExecuteAsyncTasks() override; private: + void onThink_async(); + auto getTargetIterator(const std::shared_ptr &creature) { return std::ranges::find_if(targetList.begin(), targetList.end(), [id = creature->getID()](const std::weak_ptr &ref) { const auto &target = ref.lock(); diff --git a/src/game/game.cpp b/src/game/game.cpp index 4dc0b9e3e20..5c7dc7aaf16 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6457,8 +6457,18 @@ void Game::checkCreatures() { if (const auto creature = weak.lock()) { if (creature->creatureCheck && creature->isAlive()) { creature->onThink(EVENT_CREATURE_THINK_INTERVAL); - creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); - creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + if (creature->getMonster()) { + // The monster's onThink is executed asynchronously, + // so the target is updated later, so we need to postpone the actions below. + g_dispatcher().addEvent([creature] { + if (creature->isAlive()) { + creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); + creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + } }, __FUNCTION__); + } else { + creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); + creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + } return false; } From ba62a415a6e9f819b852925e005e99e04a7f63db Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Wed, 11 Dec 2024 00:38:22 -0300 Subject: [PATCH 09/24] improve: migration database update (#3071) This improves the database migration process by removing the need to manually define if more migrations are required with return values in Lua scripts. The new approach automatically processes all migration files based on their version, reducing complexity and making the update process more robust. This change aims to streamline the database update logic and eliminate manual steps previously required. --- data-canary/migrations/0.lua | 3 - data-canary/migrations/1.lua | 5 -- data-canary/migrations/README.md | 45 +++++++++++++ data-otservbr-global/migrations/0.lua | 14 ---- data-otservbr-global/migrations/1.lua | 35 +++------- data-otservbr-global/migrations/10.lua | 1 - data-otservbr-global/migrations/11.lua | 1 - data-otservbr-global/migrations/12.lua | 1 - data-otservbr-global/migrations/13.lua | 1 - data-otservbr-global/migrations/14.lua | 1 - data-otservbr-global/migrations/15.lua | 1 - data-otservbr-global/migrations/16.lua | 1 - data-otservbr-global/migrations/17.lua | 1 - data-otservbr-global/migrations/18.lua | 2 - data-otservbr-global/migrations/19.lua | 2 - data-otservbr-global/migrations/2.lua | 2 - data-otservbr-global/migrations/20.lua | 1 - data-otservbr-global/migrations/21.lua | 1 - data-otservbr-global/migrations/22.lua | 1 - data-otservbr-global/migrations/23.lua | 1 - data-otservbr-global/migrations/24.lua | 1 - data-otservbr-global/migrations/25.lua | 1 - data-otservbr-global/migrations/26.lua | 1 - data-otservbr-global/migrations/27.lua | 1 - data-otservbr-global/migrations/28.lua | 1 - data-otservbr-global/migrations/29.lua | 1 - data-otservbr-global/migrations/3.lua | 1 - data-otservbr-global/migrations/30.lua | 1 - data-otservbr-global/migrations/31.lua | 1 - data-otservbr-global/migrations/32.lua | 1 - data-otservbr-global/migrations/33.lua | 1 - data-otservbr-global/migrations/34.lua | 1 - data-otservbr-global/migrations/35.lua | 1 - data-otservbr-global/migrations/36.lua | 1 - data-otservbr-global/migrations/37.lua | 1 - data-otservbr-global/migrations/38.lua | 1 - data-otservbr-global/migrations/39.lua | 1 - data-otservbr-global/migrations/4.lua | 1 - data-otservbr-global/migrations/40.lua | 2 - data-otservbr-global/migrations/41.lua | 2 - data-otservbr-global/migrations/42.lua | 2 - data-otservbr-global/migrations/43.lua | 2 - data-otservbr-global/migrations/44.lua | 2 - data-otservbr-global/migrations/45.lua | 2 - data-otservbr-global/migrations/46.lua | 2 - data-otservbr-global/migrations/47.lua | 23 ++++++- data-otservbr-global/migrations/5.lua | 1 - data-otservbr-global/migrations/6.lua | 1 - data-otservbr-global/migrations/7.lua | 1 - data-otservbr-global/migrations/8.lua | 1 - data-otservbr-global/migrations/9.lua | 1 - data-otservbr-global/migrations/README.md | 45 +++++++++++++ src/database/databasemanager.cpp | 81 +++++++++++++++-------- 53 files changed, 177 insertions(+), 129 deletions(-) delete mode 100644 data-canary/migrations/0.lua delete mode 100644 data-canary/migrations/1.lua create mode 100644 data-canary/migrations/README.md delete mode 100644 data-otservbr-global/migrations/0.lua create mode 100644 data-otservbr-global/migrations/README.md diff --git a/data-canary/migrations/0.lua b/data-canary/migrations/0.lua deleted file mode 100644 index d0ffd9c0cb3..00000000000 --- a/data-canary/migrations/0.lua +++ /dev/null @@ -1,3 +0,0 @@ -function onUpdateDatabase() - return false -end diff --git a/data-canary/migrations/1.lua b/data-canary/migrations/1.lua deleted file mode 100644 index 332f1838723..00000000000 --- a/data-canary/migrations/1.lua +++ /dev/null @@ -1,5 +0,0 @@ --- return true = There are others migrations file --- return false = This is the last migration file -function onUpdateDatabase() - return false -end diff --git a/data-canary/migrations/README.md b/data-canary/migrations/README.md new file mode 100644 index 00000000000..b23473bf29d --- /dev/null +++ b/data-canary/migrations/README.md @@ -0,0 +1,45 @@ +# Database Migration System + +This document provides an overview of the current database migration system for the project. The migration process has been streamlined to ensure that all migration scripts are automatically applied in order, making it easier to maintain database updates. + +## How It Works + +The migration system is designed to apply updates to the database schema or data whenever a new server version is started. Migration scripts are stored in the `migrations` directory, and the system will automatically apply any scripts that have not yet been executed. + +### Steps Involved + +1. **Retrieve Current Database Version**: + - The system first retrieves the current version of the database using `getDatabaseVersion()`. + - This version is used to determine which migration scripts need to be executed. + +2. **Migration Files Directory**: + - All migration scripts are stored in the `migrations` directory. + - Each migration script is named using a numerical pattern, such as `1.lua`, `2.lua`, etc. + - The naming convention helps determine the order in which scripts should be applied. + +3. **Execute Migration Scripts**: + - The migration system iterates through the migration directory and applies each migration script that has a version greater than the current database version. + - Only scripts that have not been applied are executed. + - The Lua state (`lua_State* L`) is initialized to run each script. + +4. **Update Database Version**: + - After each migration script is successfully applied, the system updates the database version to reflect the applied change. + - This ensures that the script is not re-applied on subsequent server startups. + +## Example Migration Script + +Below is an example of what a migration script might look like. Note that no return value is required, as all migration files are applied based on the current database version. + +```lua +-- Migration script example (for documentation purposes only) +-- This migration script should include all necessary SQL commands or operations to apply a specific update to the database. + +-- Example: Adding a new column to the "players" table +local query = [[ + ALTER TABLE players ADD COLUMN new_feature_flag TINYINT(1) NOT NULL DEFAULT 0; +]] + +-- Execute the query +db.execute(query) -- This function executes the given SQL query on the database. + +-- Note: Ensure that queries are validated to avoid errors during the migration process. diff --git a/data-otservbr-global/migrations/0.lua b/data-otservbr-global/migrations/0.lua deleted file mode 100644 index b40962e7c1f..00000000000 --- a/data-otservbr-global/migrations/0.lua +++ /dev/null @@ -1,14 +0,0 @@ -function onUpdateDatabase() - logger.info("Updating database to version 1 (sample players)") - -- Rook Sample - db.query("UPDATE `players` SET `level` = 2, `vocation` = 0, `health` = 155, `healthmax` = 155, `experience` = 100, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 60, `manamax` = 60, `town_id` = 1, `cap` = 410 WHERE `id` = 1;") - -- Sorcerer Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 1, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 2;") - -- Druid Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 2, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 3;") - -- Paladin Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 3, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 4;") - -- Knight Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 4, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 5;") - return true -end diff --git a/data-otservbr-global/migrations/1.lua b/data-otservbr-global/migrations/1.lua index 180a8b2ce90..1c5888f52d7 100644 --- a/data-otservbr-global/migrations/1.lua +++ b/data-otservbr-global/migrations/1.lua @@ -1,26 +1,13 @@ function onUpdateDatabase() - logger.info("Updating database to version 2 (hireling)") - - db.query([[ - CREATE TABLE IF NOT EXISTS `player_hirelings` ( - `id` INT NOT NULL PRIMARY KEY auto_increment, - `player_id` INT NOT NULL, - `name` varchar(255), - `active` tinyint unsigned NOT NULL DEFAULT '0', - `sex` tinyint unsigned NOT NULL DEFAULT '0', - `posx` int(11) NOT NULL DEFAULT '0', - `posy` int(11) NOT NULL DEFAULT '0', - `posz` int(11) NOT NULL DEFAULT '0', - `lookbody` int(11) NOT NULL DEFAULT '0', - `lookfeet` int(11) NOT NULL DEFAULT '0', - `lookhead` int(11) NOT NULL DEFAULT '0', - `looklegs` int(11) NOT NULL DEFAULT '0', - `looktype` int(11) NOT NULL DEFAULT '136', - - FOREIGN KEY(`player_id`) REFERENCES `players`(`id`) - ON DELETE CASCADE - ) - ]]) - - return true + logger.info("Updating database to version 1 (sample players)") + -- Rook Sample + db.query("UPDATE `players` SET `level` = 2, `vocation` = 0, `health` = 155, `healthmax` = 155, `experience` = 100, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 60, `manamax` = 60, `town_id` = 1, `cap` = 410 WHERE `id` = 1;") + -- Sorcerer Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 1, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 2;") + -- Druid Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 2, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 3;") + -- Paladin Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 3, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 4;") + -- Knight Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 4, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 5;") end diff --git a/data-otservbr-global/migrations/10.lua b/data-otservbr-global/migrations/10.lua index 0285bb0fee6..9dfded3813d 100644 --- a/data-otservbr-global/migrations/10.lua +++ b/data-otservbr-global/migrations/10.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 11 (Guilds Balance)") db.query("ALTER TABLE `guilds` ADD `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/11.lua b/data-otservbr-global/migrations/11.lua index 7f448f94a9d..08d40b66381 100644 --- a/data-otservbr-global/migrations/11.lua +++ b/data-otservbr-global/migrations/11.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 12 (Player get daily reward)") db.query("ALTER TABLE `players` ADD `isreward` tinyint(1) NOT NULL DEFAULT 1") - return true end diff --git a/data-otservbr-global/migrations/12.lua b/data-otservbr-global/migrations/12.lua index 0c6e27bb275..e83ca4e51f8 100644 --- a/data-otservbr-global/migrations/12.lua +++ b/data-otservbr-global/migrations/12.lua @@ -7,5 +7,4 @@ function onUpdateDatabase() db.query("ALTER TABLE boosted_creature ADD `lookbody` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `lookaddons` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `lookmount` int(11) DEFAULT 0;") - return true end diff --git a/data-otservbr-global/migrations/13.lua b/data-otservbr-global/migrations/13.lua index 5918b6cbd58..479b28eda79 100644 --- a/data-otservbr-global/migrations/13.lua +++ b/data-otservbr-global/migrations/13.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 14 (Fixed mana spent)") db.query("ALTER TABLE `players` CHANGE `manaspent` `manaspent` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/14.lua b/data-otservbr-global/migrations/14.lua index d2a1faf273b..7c23d8053b5 100644 --- a/data-otservbr-global/migrations/14.lua +++ b/data-otservbr-global/migrations/14.lua @@ -2,5 +2,4 @@ function onUpdateDatabase() logger.info("Updating database to version 15 (Magic Shield Spell)") db.query("ALTER TABLE `players` ADD `manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `skill_manaleech_amount`") db.query("ALTER TABLE `players` ADD `max_manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `manashield`") - return true end diff --git a/data-otservbr-global/migrations/15.lua b/data-otservbr-global/migrations/15.lua index 1521e96101f..73daf3c5b31 100644 --- a/data-otservbr-global/migrations/15.lua +++ b/data-otservbr-global/migrations/15.lua @@ -4,5 +4,4 @@ function onUpdateDatabase() db.query("UPDATE `players` SET `maglevel` = 2, `manaspent` = 5936, `skill_club` = 12, `skill_club_tries` = 155, `skill_sword` = 12, `skill_sword_tries` = 155, `skill_axe` = 12, `skill_axe_tries` = 155, `skill_dist` = 12, `skill_dist_tries` = 93 WHERE `id` = 1;") -- GOD db.query("UPDATE `players` SET `health` = 155, `healthmax` = 155, `experience` = 100, `looktype` = 75, `town_id` = 8 WHERE `id` = 6;") - return true end diff --git a/data-otservbr-global/migrations/16.lua b/data-otservbr-global/migrations/16.lua index c9ca340f0b3..a5766130bc3 100644 --- a/data-otservbr-global/migrations/16.lua +++ b/data-otservbr-global/migrations/16.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() print("Updating database to version 17 (Tutorial support)") db.query("ALTER TABLE `players` ADD `istutorial` SMALLINT(1) NOT NULL DEFAULT '0'") - return true -- true = There are others migrations file | false = this is the last migration file end diff --git a/data-otservbr-global/migrations/17.lua b/data-otservbr-global/migrations/17.lua index d25e4ccd5a9..9d5f0d8d624 100644 --- a/data-otservbr-global/migrations/17.lua +++ b/data-otservbr-global/migrations/17.lua @@ -2,5 +2,4 @@ function onUpdateDatabase() logger.info("Updating database to version 18 (Fix guild creation myaac)") db.query("ALTER TABLE `guilds` ADD `level` int(11) NOT NULL DEFAULT 1") db.query("ALTER TABLE `guilds` ADD `points` int(11) NOT NULL DEFAULT 0") - return true end diff --git a/data-otservbr-global/migrations/18.lua b/data-otservbr-global/migrations/18.lua index 01ef6048033..e017b86e05b 100644 --- a/data-otservbr-global/migrations/18.lua +++ b/data-otservbr-global/migrations/18.lua @@ -48,6 +48,4 @@ function onUpdateDatabase() `monster_list` BLOB NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - - return true end diff --git a/data-otservbr-global/migrations/19.lua b/data-otservbr-global/migrations/19.lua index 382273dfef1..e7d27a859ef 100644 --- a/data-otservbr-global/migrations/19.lua +++ b/data-otservbr-global/migrations/19.lua @@ -4,6 +4,4 @@ function onUpdateDatabase() db.query("ALTER TABLE `accounts` ADD `tournament_coins` int(11) NOT NULL DEFAULT 0 AFTER `coins`") db.query("ALTER TABLE `store_history` ADD `coin_type` tinyint(1) NOT NULL DEFAULT 0 AFTER `description`") db.query("ALTER TABLE `store_history` DROP COLUMN `coins`") -- Not in use anywhere. - - return true end diff --git a/data-otservbr-global/migrations/2.lua b/data-otservbr-global/migrations/2.lua index e953d579a3e..72c797b4b0e 100644 --- a/data-otservbr-global/migrations/2.lua +++ b/data-otservbr-global/migrations/2.lua @@ -109,6 +109,4 @@ function onUpdateDatabase() ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - - return true end diff --git a/data-otservbr-global/migrations/20.lua b/data-otservbr-global/migrations/20.lua index 49bc9ecd56f..daaefc6f041 100644 --- a/data-otservbr-global/migrations/20.lua +++ b/data-otservbr-global/migrations/20.lua @@ -2,5 +2,4 @@ function onUpdateDatabase() logger.info("Updating database to version 21 (Fix market price size)") db.query("ALTER TABLE `market_history` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `market_offers` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/21.lua b/data-otservbr-global/migrations/21.lua index a2e945576f7..cec635ed937 100644 --- a/data-otservbr-global/migrations/21.lua +++ b/data-otservbr-global/migrations/21.lua @@ -4,5 +4,4 @@ function onUpdateDatabase() db.query("ALTER TABLE `market_history` ADD `tier` tinyint UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `players` ADD `forge_dusts` bigint(21) NOT NULL DEFAULT '0';") db.query("ALTER TABLE `players` ADD `forge_dust_level` bigint(21) UNSIGNED NOT NULL DEFAULT '100';") - return true end diff --git a/data-otservbr-global/migrations/22.lua b/data-otservbr-global/migrations/22.lua index ee1caf87ac3..c4c5bd385bc 100644 --- a/data-otservbr-global/migrations/22.lua +++ b/data-otservbr-global/migrations/22.lua @@ -4,5 +4,4 @@ function onUpdateDatabase() ALTER TABLE `players` MODIFY offlinetraining_skill tinyint(2) NOT NULL DEFAULT '-1'; ]]) - return true end diff --git a/data-otservbr-global/migrations/23.lua b/data-otservbr-global/migrations/23.lua index da05835f759..8edac8cef47 100644 --- a/data-otservbr-global/migrations/23.lua +++ b/data-otservbr-global/migrations/23.lua @@ -16,5 +16,4 @@ function onUpdateDatabase() FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otservbr-global/migrations/24.lua b/data-otservbr-global/migrations/24.lua index c3cc1563b56..fed9f189085 100644 --- a/data-otservbr-global/migrations/24.lua +++ b/data-otservbr-global/migrations/24.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 25 (random mount outfit window)") db.query("ALTER TABLE `players` ADD `randomize_mount` SMALLINT(1) NOT NULL DEFAULT '0'") - return true end diff --git a/data-otservbr-global/migrations/25.lua b/data-otservbr-global/migrations/25.lua index e486b9ddf76..4d229bb58e5 100644 --- a/data-otservbr-global/migrations/25.lua +++ b/data-otservbr-global/migrations/25.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 26 (reward bag fix)") db.query("UPDATE player_rewards SET pid = 0 WHERE itemtype = 19202;") - return true end diff --git a/data-otservbr-global/migrations/26.lua b/data-otservbr-global/migrations/26.lua index bbac31b8170..ddf821ca5cd 100644 --- a/data-otservbr-global/migrations/26.lua +++ b/data-otservbr-global/migrations/26.lua @@ -11,5 +11,4 @@ function onUpdateDatabase() PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`)) ]]) - return true end diff --git a/data-otservbr-global/migrations/27.lua b/data-otservbr-global/migrations/27.lua index b16b589da87..73e7bf5c4f7 100644 --- a/data-otservbr-global/migrations/27.lua +++ b/data-otservbr-global/migrations/27.lua @@ -23,5 +23,4 @@ function onUpdateDatabase() `bossIdSlotTwo` int NOT NULL DEFAULT 0, `removeTimes` int NOT NULL DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) - return true end diff --git a/data-otservbr-global/migrations/28.lua b/data-otservbr-global/migrations/28.lua index 2bd44799cfc..d7575edf3c8 100644 --- a/data-otservbr-global/migrations/28.lua +++ b/data-otservbr-global/migrations/28.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 29 (transfer coins)") db.query("ALTER TABLE `accounts` ADD `coins_transferable` int unsigned NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/29.lua b/data-otservbr-global/migrations/29.lua index 1b490d324c2..0c633e46b5d 100644 --- a/data-otservbr-global/migrations/29.lua +++ b/data-otservbr-global/migrations/29.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 30 (looktypeEx)") db.query("ALTER TABLE `boosted_boss` ADD `looktypeEx` int unsigned NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/3.lua b/data-otservbr-global/migrations/3.lua index 72fc0d41cd8..ae06343be07 100644 --- a/data-otservbr-global/migrations/3.lua +++ b/data-otservbr-global/migrations/3.lua @@ -5,5 +5,4 @@ function onUpdateDatabase() ALTER TABLE `prey_slots` ADD `tick` smallint(3) NOT NULL DEFAULT '0'; ]]) - return true end diff --git a/data-otservbr-global/migrations/30.lua b/data-otservbr-global/migrations/30.lua index f724284e258..4ee1632421d 100644 --- a/data-otservbr-global/migrations/30.lua +++ b/data-otservbr-global/migrations/30.lua @@ -3,5 +3,4 @@ function onUpdateDatabase() db.query([[ ALTER TABLE `accounts` ADD COLUMN `premdays_purchased` int(11) NOT NULL DEFAULT 0; ]]) - return true end diff --git a/data-otservbr-global/migrations/31.lua b/data-otservbr-global/migrations/31.lua index 4207776b90e..5ba21bbe561 100644 --- a/data-otservbr-global/migrations/31.lua +++ b/data-otservbr-global/migrations/31.lua @@ -13,5 +13,4 @@ function onUpdateDatabase() db.query([[ ALTER TABLE `accounts` MODIFY `password` TEXT NOT NULL; ]]) - return true end diff --git a/data-otservbr-global/migrations/32.lua b/data-otservbr-global/migrations/32.lua index 102a9aafd9f..078ef407da6 100644 --- a/data-otservbr-global/migrations/32.lua +++ b/data-otservbr-global/migrations/32.lua @@ -10,5 +10,4 @@ function onUpdateDatabase() ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otservbr-global/migrations/33.lua b/data-otservbr-global/migrations/33.lua index afac0cebdfc..2c77cbb6e24 100644 --- a/data-otservbr-global/migrations/33.lua +++ b/data-otservbr-global/migrations/33.lua @@ -24,5 +24,4 @@ function onUpdateDatabase() ALTER TABLE `player_wheeldata` ADD PRIMARY KEY (`player_id`); ]]) - return true end diff --git a/data-otservbr-global/migrations/34.lua b/data-otservbr-global/migrations/34.lua index 7f0a289f656..7537f6e6582 100644 --- a/data-otservbr-global/migrations/34.lua +++ b/data-otservbr-global/migrations/34.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 35 (bosstiary tracker)") db.query("ALTER TABLE `player_bosstiary` ADD `tracker` blob NOT NULL;") - return true end diff --git a/data-otservbr-global/migrations/35.lua b/data-otservbr-global/migrations/35.lua index 10bddc47138..70c820c32fd 100644 --- a/data-otservbr-global/migrations/35.lua +++ b/data-otservbr-global/migrations/35.lua @@ -12,7 +12,6 @@ function onUpdateDatabase() until not Result.next(resultQuery) Result.free(resultQuery) end - return true end function getNewValue(premDays, lastDay) diff --git a/data-otservbr-global/migrations/36.lua b/data-otservbr-global/migrations/36.lua index 7d35e223cf9..5f912763cc8 100644 --- a/data-otservbr-global/migrations/36.lua +++ b/data-otservbr-global/migrations/36.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 37 (add coin_type to accounts)") db.query("ALTER TABLE `coins_transactions` ADD `coin_type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1';") - return true end diff --git a/data-otservbr-global/migrations/37.lua b/data-otservbr-global/migrations/37.lua index 70f2ec126ce..ae3dbefbc27 100644 --- a/data-otservbr-global/migrations/37.lua +++ b/data-otservbr-global/migrations/37.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 38 (add pronoun to players)") db.query("ALTER TABLE `players` ADD `pronoun` int(11) NOT NULL DEFAULT '0';") - return true end diff --git a/data-otservbr-global/migrations/38.lua b/data-otservbr-global/migrations/38.lua index 7981d5d7063..3412e4d488f 100644 --- a/data-otservbr-global/migrations/38.lua +++ b/data-otservbr-global/migrations/38.lua @@ -8,5 +8,4 @@ function onUpdateDatabase() PRIMARY KEY (`key_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otservbr-global/migrations/39.lua b/data-otservbr-global/migrations/39.lua index 2bf3815016f..181994882db 100644 --- a/data-otservbr-global/migrations/39.lua +++ b/data-otservbr-global/migrations/39.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 40 (house transfer ownership on startup)") db.query("ALTER TABLE `houses` ADD `new_owner` int(11) NOT NULL DEFAULT '-1';") - return true end diff --git a/data-otservbr-global/migrations/4.lua b/data-otservbr-global/migrations/4.lua index fa07383aeaa..891bc20915a 100644 --- a/data-otservbr-global/migrations/4.lua +++ b/data-otservbr-global/migrations/4.lua @@ -6,5 +6,4 @@ function onUpdateDatabase() `raceid` varchar(250) NOT NULL DEFAULT '', PRIMARY KEY (`date`) ) AS SELECT 0 AS date, "default" AS boostname, 0 AS raceid]]) - return true end diff --git a/data-otservbr-global/migrations/40.lua b/data-otservbr-global/migrations/40.lua index 22bfea0da95..be8794e7b25 100644 --- a/data-otservbr-global/migrations/40.lua +++ b/data-otservbr-global/migrations/40.lua @@ -12,6 +12,4 @@ function onUpdateDatabase() ALTER TABLE `house_lists` MODIFY `version` bigint(20) NOT NULL DEFAULT '0'; ]]) - - return true end diff --git a/data-otservbr-global/migrations/41.lua b/data-otservbr-global/migrations/41.lua index 15eb1d88e99..1fa9a40e36d 100644 --- a/data-otservbr-global/migrations/41.lua +++ b/data-otservbr-global/migrations/41.lua @@ -6,6 +6,4 @@ function onUpdateDatabase() MODIFY `xpboost_stamina` smallint(5) UNSIGNED DEFAULT NULL, MODIFY `xpboost_value` tinyint(4) UNSIGNED DEFAULT NULL ]]) - - return true end diff --git a/data-otservbr-global/migrations/42.lua b/data-otservbr-global/migrations/42.lua index 4d07b663daa..4b0b97b9987 100644 --- a/data-otservbr-global/migrations/42.lua +++ b/data-otservbr-global/migrations/42.lua @@ -5,6 +5,4 @@ function onUpdateDatabase() ALTER TABLE `guildwar_kills` DROP INDEX `guildwar_kills_unique` ]]) - - return true end diff --git a/data-otservbr-global/migrations/43.lua b/data-otservbr-global/migrations/43.lua index 6d3492a815a..438ba91b713 100644 --- a/data-otservbr-global/migrations/43.lua +++ b/data-otservbr-global/migrations/43.lua @@ -7,6 +7,4 @@ function onUpdateDatabase() ADD `payment` bigint(13) UNSIGNED NOT NULL DEFAULT '0', ADD `duration_days` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' ]]) - - return true end diff --git a/data-otservbr-global/migrations/44.lua b/data-otservbr-global/migrations/44.lua index c551fc79aeb..0e2140c3183 100644 --- a/data-otservbr-global/migrations/44.lua +++ b/data-otservbr-global/migrations/44.lua @@ -6,6 +6,4 @@ function onUpdateDatabase() MODIFY COLUMN `manashield` INT UNSIGNED NOT NULL DEFAULT '0', MODIFY COLUMN `max_manashield` INT UNSIGNED NOT NULL DEFAULT '0'; ]]) - - return true end diff --git a/data-otservbr-global/migrations/45.lua b/data-otservbr-global/migrations/45.lua index 4ceb5f7e3fd..a88de886861 100644 --- a/data-otservbr-global/migrations/45.lua +++ b/data-otservbr-global/migrations/45.lua @@ -51,6 +51,4 @@ function onUpdateDatabase() INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) SELECT 3, id, 'Trading Partners', 0 FROM `accounts`; ]]) - - return true end diff --git a/data-otservbr-global/migrations/46.lua b/data-otservbr-global/migrations/46.lua index 506da3a132b..da4ade8cbf3 100644 --- a/data-otservbr-global/migrations/46.lua +++ b/data-otservbr-global/migrations/46.lua @@ -2,6 +2,4 @@ function onUpdateDatabase() logger.info("Updating database to version 47 (fix: creature speed and conditions)") db.query("ALTER TABLE `players` MODIFY `conditions` mediumblob NOT NULL;") - - return true end diff --git a/data-otservbr-global/migrations/47.lua b/data-otservbr-global/migrations/47.lua index 86a6d8ffec1..3c8908b5641 100644 --- a/data-otservbr-global/migrations/47.lua +++ b/data-otservbr-global/migrations/47.lua @@ -1,3 +1,24 @@ function onUpdateDatabase() - return false -- true = There are others migrations file | false = this is the last migration file + logger.info("Updating database to version 46 (hireling)") + + db.query([[ + CREATE TABLE IF NOT EXISTS `player_hirelings` ( + `id` INT NOT NULL PRIMARY KEY auto_increment, + `player_id` INT NOT NULL, + `name` varchar(255), + `active` tinyint unsigned NOT NULL DEFAULT '0', + `sex` tinyint unsigned NOT NULL DEFAULT '0', + `posx` int(11) NOT NULL DEFAULT '0', + `posy` int(11) NOT NULL DEFAULT '0', + `posz` int(11) NOT NULL DEFAULT '0', + `lookbody` int(11) NOT NULL DEFAULT '0', + `lookfeet` int(11) NOT NULL DEFAULT '0', + `lookhead` int(11) NOT NULL DEFAULT '0', + `looklegs` int(11) NOT NULL DEFAULT '0', + `looktype` int(11) NOT NULL DEFAULT '136', + + FOREIGN KEY(`player_id`) REFERENCES `players`(`id`) + ON DELETE CASCADE + ) + ]]) end diff --git a/data-otservbr-global/migrations/5.lua b/data-otservbr-global/migrations/5.lua index 50a02a7feb5..dbc324198dd 100644 --- a/data-otservbr-global/migrations/5.lua +++ b/data-otservbr-global/migrations/5.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 6 (quickloot)") db.query("ALTER TABLE `players` ADD `quickloot_fallback` TINYINT DEFAULT 0") - return true end diff --git a/data-otservbr-global/migrations/6.lua b/data-otservbr-global/migrations/6.lua index 905a02ac146..91766a68ca7 100644 --- a/data-otservbr-global/migrations/6.lua +++ b/data-otservbr-global/migrations/6.lua @@ -4,5 +4,4 @@ function onUpdateDatabase() `player_id` INT(16) NOT NULL, `item_id` INT(16) NOT NULL, `item_count` INT(32) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) - return true -- true = There are others migrations file | false = this is the last migration file end diff --git a/data-otservbr-global/migrations/7.lua b/data-otservbr-global/migrations/7.lua index f5c7e6ed530..cade1faae1f 100644 --- a/data-otservbr-global/migrations/7.lua +++ b/data-otservbr-global/migrations/7.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() logger.info("Updating database to version 8 (recruiter system)") db.query("ALTER TABLE `accounts` ADD `recruiter` INT(6) DEFAULT 0") - return true end diff --git a/data-otservbr-global/migrations/8.lua b/data-otservbr-global/migrations/8.lua index bab5e908756..523b705c0c7 100644 --- a/data-otservbr-global/migrations/8.lua +++ b/data-otservbr-global/migrations/8.lua @@ -26,5 +26,4 @@ function onUpdateDatabase() `UsedRunesBit` VARCHAR(250) NULL , `UnlockedRunesBit` VARCHAR(250) NULL, `tracker list` BLOB NULL ) ENGINE = InnoDB DEFAULT CHARSET=utf8;]]) - return true -- true = There are others migrations file | false = this is the last migration file end diff --git a/data-otservbr-global/migrations/9.lua b/data-otservbr-global/migrations/9.lua index 23aa28daf75..7ce8e189768 100644 --- a/data-otservbr-global/migrations/9.lua +++ b/data-otservbr-global/migrations/9.lua @@ -5,5 +5,4 @@ function onUpdateDatabase() db.query("ALTER TABLE `players` ADD `lookmounthead` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmountlegs` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookfamiliarstype` int(11) unsigned NOT NULL DEFAULT '0'") - return true end diff --git a/data-otservbr-global/migrations/README.md b/data-otservbr-global/migrations/README.md new file mode 100644 index 00000000000..b23473bf29d --- /dev/null +++ b/data-otservbr-global/migrations/README.md @@ -0,0 +1,45 @@ +# Database Migration System + +This document provides an overview of the current database migration system for the project. The migration process has been streamlined to ensure that all migration scripts are automatically applied in order, making it easier to maintain database updates. + +## How It Works + +The migration system is designed to apply updates to the database schema or data whenever a new server version is started. Migration scripts are stored in the `migrations` directory, and the system will automatically apply any scripts that have not yet been executed. + +### Steps Involved + +1. **Retrieve Current Database Version**: + - The system first retrieves the current version of the database using `getDatabaseVersion()`. + - This version is used to determine which migration scripts need to be executed. + +2. **Migration Files Directory**: + - All migration scripts are stored in the `migrations` directory. + - Each migration script is named using a numerical pattern, such as `1.lua`, `2.lua`, etc. + - The naming convention helps determine the order in which scripts should be applied. + +3. **Execute Migration Scripts**: + - The migration system iterates through the migration directory and applies each migration script that has a version greater than the current database version. + - Only scripts that have not been applied are executed. + - The Lua state (`lua_State* L`) is initialized to run each script. + +4. **Update Database Version**: + - After each migration script is successfully applied, the system updates the database version to reflect the applied change. + - This ensures that the script is not re-applied on subsequent server startups. + +## Example Migration Script + +Below is an example of what a migration script might look like. Note that no return value is required, as all migration files are applied based on the current database version. + +```lua +-- Migration script example (for documentation purposes only) +-- This migration script should include all necessary SQL commands or operations to apply a specific update to the database. + +-- Example: Adding a new column to the "players" table +local query = [[ + ALTER TABLE players ADD COLUMN new_feature_flag TINYINT(1) NOT NULL DEFAULT 0; +]] + +-- Execute the query +db.execute(query) -- This function executes the given SQL query on the database. + +-- Note: Ensure that queries are validated to avoid errors during the migration process. diff --git a/src/database/databasemanager.cpp b/src/database/databasemanager.cpp index 2941172f35a..142a1db332a 100644 --- a/src/database/databasemanager.cpp +++ b/src/database/databasemanager.cpp @@ -13,6 +13,19 @@ #include "lua/functions/core/libs/core_libs_functions.hpp" #include "lua/scripts/luascript.hpp" +namespace InternalDBManager { + int32_t extractVersionFromFilename(const std::string &filename) { + std::regex versionRegex(R"((\d+)\.lua)"); + std::smatch match; + + if (std::regex_search(filename, match, versionRegex) && match.size() > 1) { + return std::stoi(match.str(1)); + } + + return -1; + } +} + bool DatabaseManager::optimizeTables() { Database &db = Database::getInstance(); std::ostringstream query; @@ -73,48 +86,62 @@ int32_t DatabaseManager::getDatabaseVersion() { } void DatabaseManager::updateDatabase() { + Benchmark bm; lua_State* L = luaL_newstate(); if (!L) { return; } luaL_openlibs(L); - CoreLibsFunctions::init(L); - int32_t version = getDatabaseVersion(); - do { - std::ostringstream ss; - ss << g_configManager().getString(DATA_DIRECTORY) + "/migrations/" << version << ".lua"; - if (luaL_dofile(L, ss.str().c_str()) != 0) { - g_logger().error("DatabaseManager::updateDatabase - Version: {}" - "] {}", - version, lua_tostring(L, -1)); - break; - } + int32_t currentVersion = getDatabaseVersion(); + std::string migrationDirectory = g_configManager().getString(DATA_DIRECTORY) + "/migrations/"; - if (!LuaScriptInterface::reserveScriptEnv()) { - break; - } + std::vector> migrations; - lua_getglobal(L, "onUpdateDatabase"); - if (lua_pcall(L, 0, 1, 0) != 0) { - LuaScriptInterface::resetScriptEnv(); - g_logger().warn("[DatabaseManager::updateDatabase - Version: {}] {}", version, lua_tostring(L, -1)); - break; + for (const auto &entry : std::filesystem::directory_iterator(migrationDirectory)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + int32_t fileVersion = InternalDBManager::extractVersionFromFilename(filename); + migrations.emplace_back(fileVersion, entry.path().string()); } + } + + std::sort(migrations.begin(), migrations.end()); + + for (const auto &[fileVersion, scriptPath] : migrations) { + if (fileVersion > currentVersion) { + if (!LuaScriptInterface::reserveScriptEnv()) { + break; + } + + if (luaL_dofile(L, scriptPath.c_str()) != 0) { + g_logger().error("DatabaseManager::updateDatabase - Version: {}] {}", fileVersion, lua_tostring(L, -1)); + continue; + } + + lua_getglobal(L, "onUpdateDatabase"); + if (lua_pcall(L, 0, 1, 0) != 0) { + LuaScriptInterface::resetScriptEnv(); + g_logger().warn("[DatabaseManager::updateDatabase - Version: {}] {}", fileVersion, lua_tostring(L, -1)); + continue; + } + + currentVersion = fileVersion; + g_logger().info("Database has been updated to version {}", currentVersion); + registerDatabaseConfig("db_version", currentVersion); - if (!LuaScriptInterface::getBoolean(L, -1, false)) { LuaScriptInterface::resetScriptEnv(); - break; } + } - version++; - g_logger().info("Database has been updated to version {}", version); - registerDatabaseConfig("db_version", version); - - LuaScriptInterface::resetScriptEnv(); - } while (true); + double duration = bm.duration(); + if (duration < 1000.0) { + g_logger().debug("Database update completed in {:.2f} ms", duration); + } else { + g_logger().debug("Database update completed in {:.2f} seconds", duration / 1000.0); + } lua_close(L); } From 60dd51440cd364454dac1ce5ef6197cca187d223 Mon Sep 17 00:00:00 2001 From: Marco Date: Fri, 13 Dec 2024 15:19:25 -0300 Subject: [PATCH 10/24] fix: remove unsupported compiler flags for MSVC (#3173) Resolves warnings issued during the Windows build process with MSVC caused by unsupported compiler flags `(-march, -mtune, -mno-avx, and -mno-sse4)`. --- CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d7ba93bafb2..c24705e51e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,9 +38,10 @@ endif() list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) # Configure build options for compatibility with commodity CPUs -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") - +if(NOT MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") +endif() # ***************************************************************************** # Include cmake tools From 61c1fc08abb9e9fbfecf6bcfb464a09a8214e1a1 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 14 Dec 2024 01:53:28 -0300 Subject: [PATCH 11/24] fix: infinite loop in Zone:randomPosition when no valid tile exist (#3178) This commit fixes an infinite loop issue in the `Zone:randomPosition` function. When no valid positions (walkable tiles) exist in the zone, the function would previously enter an infinite loop. The updated logic now filters all positions upfront to identify walkable tiles, and if none are found, the function returns `nil` with appropriate logging. This change ensures better error handling and prevents server freezes due to infinite loops. --- data/libs/systems/zones.lua | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/data/libs/systems/zones.lua b/data/libs/systems/zones.lua index 698a464fe87..a232a071f41 100644 --- a/data/libs/systems/zones.lua +++ b/data/libs/systems/zones.lua @@ -15,12 +15,24 @@ function Zone:randomPosition() logger.error("Zone:randomPosition() - Zone {} has no positions", self:getName()) return nil end - local destination = positions[math.random(1, #positions)] - local tile = destination:getTile() - while not tile or not tile:isWalkable(false, false, false, false, true) do - destination = positions[math.random(1, #positions)] - tile = destination:getTile() + + local validPositions = {} + for _, position in ipairs(positions) do + local tile = position:getTile() + if tile and tile:isWalkable(false, false, false, false, true) then + table.insert(validPositions, position) + else + logger.debug("Zone:randomPosition() - Position {} is invalid (Tile: {}, Walkable: {})", position, tile or "nil", tile and tile:isWalkable(false, false, false, false, true) or "false") + end end + + if #validPositions == 0 then + logger.error("Zone:randomPosition() - No valid positions in Zone {}", self:getName()) + return nil + end + + local destination = validPositions[math.random(1, #validPositions)] + logger.debug("Zone:randomPosition() - Selected valid position: {}", destination) return destination end From 03f1d1435e5460fd8ef3f277d5fe7bff91d7099c Mon Sep 17 00:00:00 2001 From: Felipe Pessoa Date: Sat, 14 Dec 2024 03:57:27 -0300 Subject: [PATCH 12/24] fix: position after try to cross bridge (#3175) The problem was that the ladder action was executed first, followed immediately by the teleportTo action, causing the Player to move above the ladder with the possibility of crossing the bridge. --- .../scripts/movements/rookgaard/rook_village.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-otservbr-global/scripts/movements/rookgaard/rook_village.lua b/data-otservbr-global/scripts/movements/rookgaard/rook_village.lua index 3b22a4c2394..b8e9b95f3bd 100644 --- a/data-otservbr-global/scripts/movements/rookgaard/rook_village.lua +++ b/data-otservbr-global/scripts/movements/rookgaard/rook_village.lua @@ -6,7 +6,7 @@ function rookVillage.onStepIn(creature, item, position, fromPosition) return true end - player:teleportTo(Position(player:getPosition().x, player:getPosition().y - 1, player:getPosition().z)) + player:teleportTo(Position(player:getPosition().x, player:getPosition().y - 3, player:getPosition().z + 1)) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You don't have any business there anymore.") return true From 05ff8be4219afb7a411e3418055c502184e50e29 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 14 Dec 2024 05:31:41 -0300 Subject: [PATCH 13/24] enhance: Monster::getDanceStep code duplication (#2997) Refactored the `getDanceStep` function to avoid code duplication by introducing a helper function `tryAddDirection`. This change improves code readability and maintainability by reducing repetitive logic. --- src/creatures/monsters/monster.cpp | 57 ++++++++---------------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index d30c2a33471..690ebd10b39 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -1581,73 +1581,44 @@ bool Monster::getDanceStep(const Position &creaturePos, Direction &moveDirection uint32_t centerToDist = std::max(distance_x, distance_y); // monsters not at targetDistance shouldn't dancestep - if (centerToDist < (uint32_t)targetDistance) { + if (centerToDist < static_cast(targetDistance)) { return false; } std::vector dirList; - if (!keepDistance || offset_y >= 0) { - uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() - 1) - centerPos.getY())); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_NORTH)) { + auto tryAddDirection = [&](Direction direction, int_fast32_t newX, int_fast32_t newY) { + uint32_t tmpDist = std::max(std::abs(newX - centerPos.getX()), std::abs(newY - centerPos.getY())); + if (tmpDist == centerToDist && canWalkTo(creaturePos, direction)) { bool result = true; if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y - 1, creaturePos.z), attackedCreature)); + result = (!canDoAttackNow || canUseAttack(Position(newX, newY, creaturePos.z), attackedCreature)); } if (result) { - dirList.push_back(DIRECTION_NORTH); + dirList.emplace_back(direction); } } + }; + + if (!keepDistance || offset_y >= 0) { + tryAddDirection(DIRECTION_NORTH, creaturePos.getX(), creaturePos.getY() - 1); } if (!keepDistance || offset_y <= 0) { - uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() + 1) - centerPos.getY())); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_SOUTH)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y + 1, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_SOUTH); - } - } + tryAddDirection(DIRECTION_SOUTH, creaturePos.getX(), creaturePos.getY() + 1); } if (!keepDistance || offset_x <= 0) { - uint32_t tmpDist = std::max(std::abs((creaturePos.getX() + 1) - centerPos.getX()), distance_y); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_EAST)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x + 1, creaturePos.y, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_EAST); - } - } + tryAddDirection(DIRECTION_EAST, creaturePos.getX() + 1, creaturePos.getY()); } if (!keepDistance || offset_x >= 0) { - uint32_t tmpDist = std::max(std::abs((creaturePos.getX() - 1) - centerPos.getX()), distance_y); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_WEST)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x - 1, creaturePos.y, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_WEST); - } - } + tryAddDirection(DIRECTION_WEST, creaturePos.getX() - 1, creaturePos.getY()); } if (!dirList.empty()) { - std::shuffle(dirList.begin(), dirList.end(), getRandomGenerator()); + std::ranges::shuffle(dirList, getRandomGenerator()); moveDirection = dirList[uniform_random(0, dirList.size() - 1)]; return true; } From f2750bb8eac8d95c26788b15619210fea8e6b3d2 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 20 Dec 2024 23:58:41 -0300 Subject: [PATCH 14/24] fix: hazard spawn initialization (#3184) Fixes bugs introduced here: https://github.com/opentibiabr/canary/pull/3076 By mistake I removed the onSpawn from the hazard and didn't realize it. --- data/libs/systems/hazard.lua | 12 ++++++------ data/scripts/lib/register_monster_type.lua | 6 +++++- src/creatures/monsters/monster.cpp | 7 ++++--- src/creatures/monsters/monster.hpp | 2 +- src/creatures/monsters/spawns/spawn_monster.cpp | 2 +- src/lua/functions/core/game/game_functions.cpp | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/data/libs/systems/hazard.lua b/data/libs/systems/hazard.lua index a1501b2d1fd..387aef874c2 100644 --- a/data/libs/systems/hazard.lua +++ b/data/libs/systems/hazard.lua @@ -193,16 +193,16 @@ function HazardMonster.onSpawn(monster, position) if not zones then return true end + + logger.debug("Monster {} spawned in hazard zone, position {}", monster:getName(), position:toString()) for _, zone in ipairs(zones) do local hazard = Hazard.getByName(zone:getName()) if hazard then monster:hazard(true) - if hazard then - monster:hazardCrit(hazard.crit) - monster:hazardDodge(hazard.dodge) - monster:hazardDamageBoost(hazard.damageBoost) - monster:hazardDefenseBoost(hazard.defenseBoost) - end + monster:hazardCrit(hazard.crit) + monster:hazardDodge(hazard.dodge) + monster:hazardDamageBoost(hazard.damageBoost) + monster:hazardDefenseBoost(hazard.defenseBoost) end end return true diff --git a/data/scripts/lib/register_monster_type.lua b/data/scripts/lib/register_monster_type.lua index ed5de6421d9..81960f4f84d 100644 --- a/data/scripts/lib/register_monster_type.lua +++ b/data/scripts/lib/register_monster_type.lua @@ -17,6 +17,10 @@ end registerMonsterType.name = function(mtype, mask) if mask.name then mtype:name(mask.name) + -- Try register hazard monsters + mtype.onSpawn = function(monster, spawnPosition) + HazardMonster.onSpawn(monster, spawnPosition) + end end end registerMonsterType.description = function(mtype, mask) @@ -194,7 +198,7 @@ registerMonsterType.flags = function(mtype, mask) end if mask.flags.rewardBoss then mtype:isRewardBoss(mask.flags.rewardBoss) - mtype.onSpawn = function(monster) + mtype.onSpawn = function(monster, spawnPosition) monster:setRewardBoss() end end diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 690ebd10b39..fed817af239 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -501,9 +501,9 @@ void Monster::onAttackedByPlayer(const std::shared_ptr &attackerPlayer) } } -void Monster::onSpawn() { +void Monster::onSpawn(const Position &position) { if (mType->info.spawnEvent != -1) { - // onSpawn(self) + // onSpawn(self, spawnPosition) LuaScriptInterface* scriptInterface = mType->info.scriptInterface; if (!scriptInterface->reserveScriptEnv()) { g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " @@ -520,8 +520,9 @@ void Monster::onSpawn() { LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); + LuaScriptInterface::pushPosition(L, position); - scriptInterface->callVoidFunction(1); + scriptInterface->callVoidFunction(2); } } diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 145b973fdc9..6e44d8f36bf 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -91,7 +91,7 @@ class Monster final : public Creature { void onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) override; void onCreatureSay(const std::shared_ptr &creature, SpeakClasses type, const std::string &text) override; void onAttackedByPlayer(const std::shared_ptr &attackerPlayer); - void onSpawn(); + void onSpawn(const Position &position); void drainHealth(const std::shared_ptr &attacker, int32_t damage) override; void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index 3a915d737b2..b80266f56ca 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -232,7 +232,7 @@ bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const spawnedMonsterMap[spawnMonsterId] = monster; sb.lastSpawn = OTSYS_TIME(); - monster->onSpawn(); + monster->onSpawn(sb.pos); return true; } diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index aad4b71d06c..eec876eee7c 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -524,7 +524,7 @@ int GameFunctions::luaGameCreateMonster(lua_State* L) { const bool extended = Lua::getBoolean(L, 3, false); const bool force = Lua::getBoolean(L, 4, false); if (g_game().placeCreature(monster, position, extended, force)) { - monster->onSpawn(); + monster->onSpawn(position); const auto &mtype = monster->getMonsterType(); if (mtype && mtype->info.raceid > 0 && mtype->info.bosstiaryRace == BosstiaryRarity_t::RARITY_ARCHFOE) { for (const auto &spectator : Spectators().find(monster->getPosition(), true)) { From 13ae9e55224421bbb1a01eaa1706042a0961da80 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 21 Dec 2024 00:47:32 -0300 Subject: [PATCH 15/24] fix: suppress get byte log (#3185) Resolves #3153 This simple "suppress" the "getByte" log, some locations send "0" by default, so we don't need to send log in these locations. --- src/server/network/message/networkmessage.cpp | 8 +++++--- src/server/network/message/networkmessage.hpp | 2 +- src/server/network/protocol/protocolgame.cpp | 16 ++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index 0fb8b63324f..4f203dc4551 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -45,10 +45,12 @@ int32_t NetworkMessage::decodeHeader() { } // Simply read functions for incoming message -uint8_t NetworkMessage::getByte(const std::source_location &location /*= std::source_location::current()*/) { +uint8_t NetworkMessage::getByte(bool suppresLog /*= false*/, const std::source_location &location /*= std::source_location::current()*/) { // Check if there is at least 1 byte to read if (!canRead(1)) { - g_logger().error("[{}] Not enough data to read a byte. Current position: {}, Length: {}. Called line {}:{} in {}", __FUNCTION__, info.position, info.length, location.line(), location.column(), location.function_name()); + if (!suppresLog) { + g_logger().error("[{}] Not enough data to read a byte. Current position: {}, Length: {}. Called line {}:{} in {}", __FUNCTION__, info.position, info.length, location.line(), location.column(), location.function_name()); + } return {}; } @@ -113,7 +115,7 @@ Position NetworkMessage::getPosition() { Position pos; pos.x = get(); pos.y = get(); - pos.z = getByte(); + pos.z = getByte(true); return pos; } diff --git a/src/server/network/message/networkmessage.hpp b/src/server/network/message/networkmessage.hpp index f738de8506b..6549eec8ea5 100644 --- a/src/server/network/message/networkmessage.hpp +++ b/src/server/network/message/networkmessage.hpp @@ -34,7 +34,7 @@ class NetworkMessage { } // simply read functions for incoming message - uint8_t getByte(const std::source_location &location = std::source_location::current()); + uint8_t getByte(bool suppresLog = false, const std::source_location &location = std::source_location::current()); uint8_t getPreviousByte(); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 9c09f5ead38..dc02e316f1f 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -1713,14 +1713,14 @@ void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { } void ProtocolGame::parseToggleMount(NetworkMessage &msg) { - bool mount = msg.getByte() != 0; + bool mount = msg.getByte(true) != 0; g_game().playerToggleMount(player->getID(), mount); } void ProtocolGame::parseApplyImbuement(NetworkMessage &msg) { uint8_t slot = msg.getByte(); auto imbuementId = msg.get(); - bool protectionCharm = msg.getByte() != 0x00; + bool protectionCharm = msg.getByte(true) != 0x00; g_game().playerApplyImbuement(player->getID(), imbuementId, slot, protectionCharm); } @@ -1967,8 +1967,8 @@ void ProtocolGame::parsePlayerBuyOnShop(NetworkMessage &msg) { auto id = msg.get(); uint8_t count = msg.getByte(); uint16_t amount = oldProtocol ? static_cast(msg.getByte()) : msg.get(); - bool ignoreCap = msg.getByte() != 0; - bool inBackpacks = msg.getByte() != 0; + bool ignoreCap = msg.getByte(true) != 0; + bool inBackpacks = msg.getByte(true) != 0; g_game().playerBuyItem(player->getID(), id, count, amount, ignoreCap, inBackpacks); } @@ -1976,7 +1976,7 @@ void ProtocolGame::parsePlayerSellOnShop(NetworkMessage &msg) { auto id = msg.get(); uint8_t count = std::max(msg.getByte(), (uint8_t)1); uint16_t amount = oldProtocol ? static_cast(msg.getByte()) : msg.get(); - bool ignoreEquipped = msg.getByte() != 0; + bool ignoreEquipped = msg.getByte(true) != 0; g_game().playerSellItem(player->getID(), id, count, amount, ignoreEquipped); } @@ -2010,7 +2010,7 @@ void ProtocolGame::parseEditVip(NetworkMessage &msg) { auto guid = msg.get(); const std::string description = msg.getString(); uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63 - bool notify = msg.getByte() != 0; + bool notify = msg.getByte(true) != 0; uint8_t groupsAmount = msg.getByte(); for (uint8_t i = 0; i < groupsAmount; ++i) { uint8_t groupId = msg.getByte(); @@ -2176,7 +2176,7 @@ void ProtocolGame::parseTaskHuntingAction(NetworkMessage &msg) { uint8_t slot = msg.getByte(); uint8_t action = msg.getByte(); - bool upgrade = msg.getByte() != 0; + bool upgrade = msg.getByte(true) != 0; auto raceId = msg.get(); if (!g_configManager().getBoolean(TASK_HUNTING_ENABLED)) { @@ -3141,7 +3141,7 @@ void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) { auto amount = msg.get(); uint64_t price = oldProtocol ? static_cast(msg.get()) : msg.get(); - bool anonymous = (msg.getByte() != 0); + bool anonymous = (msg.getByte(true) != 0); if (amount > 0 && price > 0) { g_game().playerCreateMarketOffer(player->getID(), type, itemId, amount, price, itemTier, anonymous); } From ba2c41f7ff952b3db971b65738b3d4fc49a2193c Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Sat, 21 Dec 2024 02:14:16 -0300 Subject: [PATCH 16/24] fix: death call several times (#3186) fix #3177 --- src/creatures/creature.cpp | 4 ++++ src/creatures/creature.hpp | 6 +++++- src/creatures/monsters/monster.cpp | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 586f5b8c1b2..4d02f54c364 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -711,6 +711,10 @@ std::shared_ptr Creature::getCorpse(const std::shared_ptr &, con } void Creature::changeHealth(int32_t healthChange, bool sendHealthChange /* = true*/) { + if (isLifeless()) { + return; + } + int32_t oldHealth = health; if (healthChange > 0) { diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 35f39292609..bea95313f8a 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -209,7 +209,11 @@ class Creature : virtual public Thing, public SharedObject { } bool isAlive() const { - return !isDead(); + return !isLifeless(); + } + + bool isLifeless() const { + return health <= 0; } virtual int32_t getMaxHealth() const { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index fed817af239..13b30321a21 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -1110,7 +1110,7 @@ void Monster::onThink_async() { void Monster::doAttacking(uint32_t interval) { const auto &attackedCreature = getAttackedCreature(); - if (!attackedCreature || (isSummon() && attackedCreature.get() == this)) { + if (!attackedCreature || attackedCreature->isLifeless() || (isSummon() && attackedCreature.get() == this)) { return; } From 145da70d0136e837b4d4926d079471b784e9598b Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 21 Dec 2024 18:46:57 -0300 Subject: [PATCH 17/24] fix: warning on decode lenght (#3188) Resolves #3002 --- src/security/rsa.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/security/rsa.cpp b/src/security/rsa.cpp index 18e1f6e8393..f148ae6d952 100644 --- a/src/security/rsa.cpp +++ b/src/security/rsa.cpp @@ -170,11 +170,17 @@ uint16_t RSA::decodeLength(char*&pos) const { if (length & 0x80) { uint8_t numLengthBytes = length & 0x7F; if (numLengthBytes > 4) { - g_logger().error("[RSA::loadPEM] - Invalid 'length'"); + g_logger().error("[RSA::decodeLength] - Invalid 'length'"); return 0; } - // Copy 'numLengthBytes' bytes from 'pos' into 'buffer', starting at the correct position - std::ranges::copy_n(pos, numLengthBytes, buffer.begin() + (4 - numLengthBytes)); + // Adjust the copy destination to ensure it doesn't overflow + auto destIt = buffer.begin() + (4 - numLengthBytes); + if (destIt < buffer.begin() || destIt + numLengthBytes > buffer.end()) { + g_logger().error("[RSA::decodeLength] - Invalid copy range"); + return 0; + } + // Copy 'numLengthBytes' bytes from 'pos' into 'buffer' + std::copy_n(pos, numLengthBytes, destIt); pos += numLengthBytes; // Reconstruct 'length' from 'buffer' (big-endian) uint32_t tempLength = 0; @@ -182,7 +188,7 @@ uint16_t RSA::decodeLength(char*&pos) const { tempLength = (tempLength << 8) | buffer[4 - numLengthBytes + i]; } if (tempLength > UINT16_MAX) { - g_logger().error("[RSA::loadPEM] - Length too large"); + g_logger().error("[RSA::decodeLength] - Length too large"); return 0; } length = static_cast(tempLength); From f81e5cd5faff04bbedc39c3a3dccd71ed4703787 Mon Sep 17 00:00:00 2001 From: Jean Carlo de Souza Date: Fri, 27 Dec 2024 12:25:00 -0300 Subject: [PATCH 18/24] fix: for lava tiles in entrance of The Pits of Inferno Quest (#3196) Implements an alternative solution to handle lava tiles in The Pits of Inferno Quest. The logic in register_actions.lua has been modified to transform existing lava items (ID: 21477) into the floor item (ID: 5815) using Tile:getItemById and Item:transform, instead of relying on Game.createItem. This addresses an issue where lava tiles in The Pits of Inferno Quest were not behaving as expected Resolves #3144 --- data-otservbr-global/scripts/lib/register_actions.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index a65de89e017..ec68c90e27c 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -704,7 +704,10 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) -- The Pits of Inferno Quest if toPosition == Position(32808, 32334, 11) then for i = 1, #lava do - Game.createItem(5815, 1, lava[i]) + local lavaTile = Tile(lava[i]):getItemById(21477) + if lavaTile then + lavaTile:transform(5815) + end end target:transform(3141) toPosition:sendMagicEffect(CONST_ME_SMOKE) From f8212483983c8da2e5a490822cf3650dbb4cd739 Mon Sep 17 00:00:00 2001 From: Pedro Cruz Date: Sun, 29 Dec 2024 15:21:45 -0300 Subject: [PATCH 19/24] fix: party shared experience (#3133) This fixes the party shared experience that was not providing the correct amount of experience giving the unique vocations count and party size. --- data/events/scripts/party.lua | 27 ++++++------------- src/creatures/players/grouping/party.cpp | 26 ++++++++++++++++-- src/creatures/players/grouping/party.hpp | 1 + .../creatures/player/party_functions.cpp | 12 +++++++++ .../creatures/player/party_functions.hpp | 1 + 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/data/events/scripts/party.lua b/data/events/scripts/party.lua index b27965994b8..b94613c070a 100644 --- a/data/events/scripts/party.lua +++ b/data/events/scripts/party.lua @@ -66,25 +66,14 @@ function Party:onDisband() end function Party:onShareExperience(exp) - local sharedExperienceMultiplier = 1.20 --20% - local vocationsIds = {} + local uniqueVocationsCount = self:getUniqueVocationsCount() + local partySize = self:getMemberCount() + 1 - local vocationId = self:getLeader():getVocation():getBase():getId() - if vocationId ~= VOCATION_NONE then - table.insert(vocationsIds, vocationId) - end - - for _, member in ipairs(self:getMembers()) do - vocationId = member:getVocation():getBase():getId() - if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then - table.insert(vocationsIds, vocationId) - end - end - - local size = #vocationsIds - if size > 1 then - sharedExperienceMultiplier = 1.0 + ((size * (5 * (size - 1) + 10)) / 100) - end + -- Formula to calculate the % based on the vocations amount + local sharedExperienceMultiplier = ((0.1 * (uniqueVocationsCount ^ 2)) - (0.2 * uniqueVocationsCount) + 1.3) + -- Since the formula its non linear, we need to subtract 0.1 if all vocations are present, + -- because on all vocations the multiplier is 2.1 and it should be 2.0 + sharedExperienceMultiplier = partySize < 4 and sharedExperienceMultiplier or sharedExperienceMultiplier - 0.1 - return math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1)) + return math.ceil((exp * sharedExperienceMultiplier) / partySize) end diff --git a/src/creatures/players/grouping/party.cpp b/src/creatures/players/grouping/party.cpp index e63ca14b629..c04f8d57909 100644 --- a/src/creatures/players/grouping/party.cpp +++ b/src/creatures/players/grouping/party.cpp @@ -12,6 +12,7 @@ #include "config/configmanager.hpp" #include "creatures/creature.hpp" #include "creatures/players/player.hpp" +#include "creatures/players/vocations/vocation.hpp" #include "game/game.hpp" #include "game/movement/position.hpp" #include "lua/callbacks/event_callback.hpp" @@ -61,6 +62,25 @@ size_t Party::getInvitationCount() const { return inviteList.size(); } +uint8_t Party::getUniqueVocationsCount() const { + std::unordered_set uniqueVocations; + + for (const auto &player : getPlayers()) { + if (uniqueVocations.size() >= 4) { + break; + } + + const auto &vocation = player->getVocation(); + if (!vocation) { + continue; + } + + uniqueVocations.insert(vocation->getBaseId()); + } + + return uniqueVocations.size(); +} + void Party::disband() { if (!g_events().eventPartyOnDisband(getParty())) { return; @@ -504,9 +524,11 @@ void Party::shareExperience(uint64_t experience, const std::shared_ptr g_callbacks().executeCallback(EventCallback_t::partyOnShareExperience, &EventCallback::partyOnShareExperience, getParty(), std::ref(shareExperience)); for (const auto &member : getMembers()) { - member->onGainSharedExperience(shareExperience, target); + const auto memberStaminaBoost = static_cast(member->getStaminaXpBoost()) / 100; + member->onGainSharedExperience(shareExperience * memberStaminaBoost, target); } - leader->onGainSharedExperience(shareExperience, target); + const auto leaderStaminaBoost = static_cast(leader->getStaminaXpBoost()) / 100; + leader->onGainSharedExperience(shareExperience * leaderStaminaBoost, target); } bool Party::canUseSharedExperience(const std::shared_ptr &player) { diff --git a/src/creatures/players/grouping/party.hpp b/src/creatures/players/grouping/party.hpp index cef450c0c77..bc1ba77d135 100644 --- a/src/creatures/players/grouping/party.hpp +++ b/src/creatures/players/grouping/party.hpp @@ -40,6 +40,7 @@ class Party final : public SharedObject { std::vector> getInvitees(); size_t getMemberCount() const; size_t getInvitationCount() const; + uint8_t getUniqueVocationsCount() const; void disband(); bool invitePlayer(const std::shared_ptr &player); diff --git a/src/lua/functions/creatures/player/party_functions.cpp b/src/lua/functions/creatures/player/party_functions.cpp index 5c43593282a..a44e0129b94 100644 --- a/src/lua/functions/creatures/player/party_functions.cpp +++ b/src/lua/functions/creatures/player/party_functions.cpp @@ -24,6 +24,7 @@ void PartyFunctions::init(lua_State* L) { Lua::registerMethod(L, "Party", "getMemberCount", PartyFunctions::luaPartyGetMemberCount); Lua::registerMethod(L, "Party", "getInvitees", PartyFunctions::luaPartyGetInvitees); Lua::registerMethod(L, "Party", "getInviteeCount", PartyFunctions::luaPartyGetInviteeCount); + Lua::registerMethod(L, "Party", "getUniqueVocationsCount", PartyFunctions::luaPartyGetUniqueVocationsCount); Lua::registerMethod(L, "Party", "addInvite", PartyFunctions::luaPartyAddInvite); Lua::registerMethod(L, "Party", "removeInvite", PartyFunctions::luaPartyRemoveInvite); Lua::registerMethod(L, "Party", "addMember", PartyFunctions::luaPartyAddMember); @@ -162,6 +163,17 @@ int PartyFunctions::luaPartyGetInviteeCount(lua_State* L) { return 1; } +int PartyFunctions::luaPartyGetUniqueVocationsCount(lua_State* L) { + // party:getUniqueVocationsCount() + const auto &party = Lua::getUserdataShared(L, 1); + if (party) { + lua_pushnumber(L, party->getUniqueVocationsCount()); + } else { + lua_pushnil(L); + } + return 1; +} + int PartyFunctions::luaPartyAddInvite(lua_State* L) { // party:addInvite(player) const auto &player = Lua::getPlayer(L, 2); diff --git a/src/lua/functions/creatures/player/party_functions.hpp b/src/lua/functions/creatures/player/party_functions.hpp index 52801e349d6..92e629f71a1 100644 --- a/src/lua/functions/creatures/player/party_functions.hpp +++ b/src/lua/functions/creatures/player/party_functions.hpp @@ -22,6 +22,7 @@ class PartyFunctions { static int luaPartyGetMemberCount(lua_State* L); static int luaPartyGetInvitees(lua_State* L); static int luaPartyGetInviteeCount(lua_State* L); + static int luaPartyGetUniqueVocationsCount(lua_State* L); static int luaPartyAddInvite(lua_State* L); static int luaPartyRemoveInvite(lua_State* L); static int luaPartyAddMember(lua_State* L); From cdfb178bacd5519248c843522b3d8a013e36f623 Mon Sep 17 00:00:00 2001 From: murilo09 <78226931+murilo09@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:03:35 -0300 Subject: [PATCH 20/24] feat: cyclopedia house auction (#3022) --- config.lua.dist | 4 +- data-otservbr-global/migrations/10.lua | 2 +- data-otservbr-global/migrations/11.lua | 2 +- data-otservbr-global/migrations/12.lua | 2 +- data-otservbr-global/migrations/13.lua | 2 +- data-otservbr-global/migrations/14.lua | 2 +- data-otservbr-global/migrations/15.lua | 2 +- data-otservbr-global/migrations/16.lua | 2 +- data-otservbr-global/migrations/17.lua | 2 +- data-otservbr-global/migrations/18.lua | 2 +- data-otservbr-global/migrations/19.lua | 2 +- data-otservbr-global/migrations/2.lua | 2 +- data-otservbr-global/migrations/20.lua | 2 +- data-otservbr-global/migrations/21.lua | 2 +- data-otservbr-global/migrations/22.lua | 2 +- data-otservbr-global/migrations/23.lua | 2 +- data-otservbr-global/migrations/24.lua | 2 +- data-otservbr-global/migrations/25.lua | 2 +- data-otservbr-global/migrations/26.lua | 2 +- data-otservbr-global/migrations/27.lua | 2 +- data-otservbr-global/migrations/28.lua | 2 +- data-otservbr-global/migrations/29.lua | 2 +- data-otservbr-global/migrations/3.lua | 2 +- data-otservbr-global/migrations/30.lua | 2 +- data-otservbr-global/migrations/31.lua | 2 +- data-otservbr-global/migrations/32.lua | 2 +- data-otservbr-global/migrations/33.lua | 2 +- data-otservbr-global/migrations/34.lua | 2 +- data-otservbr-global/migrations/35.lua | 2 +- data-otservbr-global/migrations/36.lua | 2 +- data-otservbr-global/migrations/37.lua | 2 +- data-otservbr-global/migrations/38.lua | 2 +- data-otservbr-global/migrations/39.lua | 2 +- data-otservbr-global/migrations/4.lua | 2 +- data-otservbr-global/migrations/40.lua | 2 +- data-otservbr-global/migrations/41.lua | 2 +- data-otservbr-global/migrations/42.lua | 2 +- data-otservbr-global/migrations/43.lua | 2 +- data-otservbr-global/migrations/44.lua | 2 +- data-otservbr-global/migrations/45.lua | 2 +- data-otservbr-global/migrations/46.lua | 2 +- data-otservbr-global/migrations/47.lua | 2 +- data-otservbr-global/migrations/48.lua | 27 + data-otservbr-global/migrations/5.lua | 2 +- data-otservbr-global/migrations/6.lua | 2 +- data-otservbr-global/migrations/7.lua | 2 +- data-otservbr-global/migrations/8.lua | 53 +- data-otservbr-global/migrations/9.lua | 2 +- data-otservbr-global/world/otservbr-house.xml | 1970 ++++++++--------- .../globalevents/server_initialization.lua | 24 - data/scripts/talkactions/player/buy_house.lua | 8 +- .../talkactions/player/leave_house.lua | 8 +- .../scripts/talkactions/player/sell_house.lua | 8 +- schema.sql | 14 +- src/account/account.cpp | 7 + src/account/account.hpp | 3 + src/account/account_info.hpp | 1 + src/account/account_repository_db.cpp | 3 +- src/config/config_enums.hpp | 2 + src/config/configmanager.cpp | 2 + src/creatures/players/player.cpp | 122 + src/creatures/players/player.hpp | 13 + src/enums/player_cyclopedia.hpp | 58 + src/game/game.cpp | 351 +++ src/game/game.hpp | 11 + src/io/iologindata.cpp | 8 - src/io/iologindata.hpp | 1 - src/io/iomapserialize.cpp | 89 +- src/lua/functions/map/house_functions.cpp | 3 +- src/map/house/house.cpp | 103 +- src/map/house/house.hpp | 113 + src/server/network/protocol/protocolgame.cpp | 194 ++ src/server/network/protocol/protocolgame.hpp | 7 + 73 files changed, 2160 insertions(+), 1137 deletions(-) create mode 100644 data-otservbr-global/migrations/48.lua diff --git a/config.lua.dist b/config.lua.dist index 7d0360d9360..7c891c84b5d 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -344,9 +344,11 @@ Setting this to false may pose risks; if a house is abandoned and contains a lar ]] -- Periods: daily/weekly/monthly/yearly/never -- Base: sqm,rent,sqm+rent +toggleCyclopediaHouseAuction = true +daysToCloseBid = 7 housePriceRentMultiplier = 0.0 housePriceEachSQM = 1000 -houseRentPeriod = "never" +houseRentPeriod = "monthly" houseRentRate = 1.0 houseOwnedByAccount = false houseBuyLevel = 100 diff --git a/data-otservbr-global/migrations/10.lua b/data-otservbr-global/migrations/10.lua index 9dfded3813d..adfa41bb535 100644 --- a/data-otservbr-global/migrations/10.lua +++ b/data-otservbr-global/migrations/10.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 11 (Guilds Balance)") + logger.info("Updating database to version 10 (Guilds Balance)") db.query("ALTER TABLE `guilds` ADD `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/11.lua b/data-otservbr-global/migrations/11.lua index 08d40b66381..a10d3d2980c 100644 --- a/data-otservbr-global/migrations/11.lua +++ b/data-otservbr-global/migrations/11.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 12 (Player get daily reward)") + logger.info("Updating database to version 11 (Player get daily reward)") db.query("ALTER TABLE `players` ADD `isreward` tinyint(1) NOT NULL DEFAULT 1") end diff --git a/data-otservbr-global/migrations/12.lua b/data-otservbr-global/migrations/12.lua index e83ca4e51f8..20a2afc4582 100644 --- a/data-otservbr-global/migrations/12.lua +++ b/data-otservbr-global/migrations/12.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 13 (Boosted Creature Outfit)") + logger.info("Updating database to version 12 (Boosted Creature Outfit)") db.query("ALTER TABLE boosted_creature ADD `looktype` int(11) NOT NULL DEFAULT 136;") db.query("ALTER TABLE boosted_creature ADD `lookfeet` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `looklegs` int(11) NOT NULL DEFAULT 0;") diff --git a/data-otservbr-global/migrations/13.lua b/data-otservbr-global/migrations/13.lua index 479b28eda79..4747efdcb96 100644 --- a/data-otservbr-global/migrations/13.lua +++ b/data-otservbr-global/migrations/13.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 14 (Fixed mana spent)") + logger.info("Updating database to version 13 (Fixed mana spent)") db.query("ALTER TABLE `players` CHANGE `manaspent` `manaspent` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/14.lua b/data-otservbr-global/migrations/14.lua index 7c23d8053b5..281eb40722a 100644 --- a/data-otservbr-global/migrations/14.lua +++ b/data-otservbr-global/migrations/14.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 15 (Magic Shield Spell)") + logger.info("Updating database to version 14 (Magic Shield Spell)") db.query("ALTER TABLE `players` ADD `manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `skill_manaleech_amount`") db.query("ALTER TABLE `players` ADD `max_manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `manashield`") end diff --git a/data-otservbr-global/migrations/15.lua b/data-otservbr-global/migrations/15.lua index 73daf3c5b31..2c4a37ba315 100644 --- a/data-otservbr-global/migrations/15.lua +++ b/data-otservbr-global/migrations/15.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 16 (Rook sample and GOD player values)") + logger.info("Updating database to version 15 (Rook sample and GOD player values)") -- Rook Sample db.query("UPDATE `players` SET `maglevel` = 2, `manaspent` = 5936, `skill_club` = 12, `skill_club_tries` = 155, `skill_sword` = 12, `skill_sword_tries` = 155, `skill_axe` = 12, `skill_axe_tries` = 155, `skill_dist` = 12, `skill_dist_tries` = 93 WHERE `id` = 1;") -- GOD diff --git a/data-otservbr-global/migrations/16.lua b/data-otservbr-global/migrations/16.lua index a5766130bc3..027f2fe9822 100644 --- a/data-otservbr-global/migrations/16.lua +++ b/data-otservbr-global/migrations/16.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - print("Updating database to version 17 (Tutorial support)") + print("Updating database to version 16 (Tutorial support)") db.query("ALTER TABLE `players` ADD `istutorial` SMALLINT(1) NOT NULL DEFAULT '0'") end diff --git a/data-otservbr-global/migrations/17.lua b/data-otservbr-global/migrations/17.lua index 9d5f0d8d624..c84d6ec8ea6 100644 --- a/data-otservbr-global/migrations/17.lua +++ b/data-otservbr-global/migrations/17.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 18 (Fix guild creation myaac)") + logger.info("Updating database to version 17 (Fix guild creation myaac)") db.query("ALTER TABLE `guilds` ADD `level` int(11) NOT NULL DEFAULT 1") db.query("ALTER TABLE `guilds` ADD `points` int(11) NOT NULL DEFAULT 0") end diff --git a/data-otservbr-global/migrations/18.lua b/data-otservbr-global/migrations/18.lua index e017b86e05b..0f5777a2678 100644 --- a/data-otservbr-global/migrations/18.lua +++ b/data-otservbr-global/migrations/18.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 19 (Prey system rework + Task hunting system)") + logger.info("Updating database to version 18 (Prey system rework + Task hunting system)") db.query([[ ALTER TABLE `players` DROP `prey_stamina_1`, diff --git a/data-otservbr-global/migrations/19.lua b/data-otservbr-global/migrations/19.lua index e7d27a859ef..dd0c82d075f 100644 --- a/data-otservbr-global/migrations/19.lua +++ b/data-otservbr-global/migrations/19.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 20 (Gamestore accepting Tournament Coins)") + logger.info("Updating database to version 19 (Gamestore accepting Tournament Coins)") db.query("ALTER TABLE `accounts` ADD `tournament_coins` int(11) NOT NULL DEFAULT 0 AFTER `coins`") db.query("ALTER TABLE `store_history` ADD `coin_type` tinyint(1) NOT NULL DEFAULT 0 AFTER `description`") diff --git a/data-otservbr-global/migrations/2.lua b/data-otservbr-global/migrations/2.lua index 72c797b4b0e..b5674fecacc 100644 --- a/data-otservbr-global/migrations/2.lua +++ b/data-otservbr-global/migrations/2.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 3 (account refactor)") + logger.info("Updating database to version 2 (account refactor)") db.query([[ LOCK TABLES diff --git a/data-otservbr-global/migrations/20.lua b/data-otservbr-global/migrations/20.lua index daaefc6f041..f232bf2a7e8 100644 --- a/data-otservbr-global/migrations/20.lua +++ b/data-otservbr-global/migrations/20.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 21 (Fix market price size)") + logger.info("Updating database to version 20 (Fix market price size)") db.query("ALTER TABLE `market_history` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `market_offers` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/21.lua b/data-otservbr-global/migrations/21.lua index cec635ed937..5c9e1a0ec8e 100644 --- a/data-otservbr-global/migrations/21.lua +++ b/data-otservbr-global/migrations/21.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 22 (forge and tier system)") + logger.info("Updating database to version 21 (forge and tier system)") db.query("ALTER TABLE `market_offers` ADD `tier` tinyint UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `market_history` ADD `tier` tinyint UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `players` ADD `forge_dusts` bigint(21) NOT NULL DEFAULT '0';") diff --git a/data-otservbr-global/migrations/22.lua b/data-otservbr-global/migrations/22.lua index c4c5bd385bc..9a2a4475a4b 100644 --- a/data-otservbr-global/migrations/22.lua +++ b/data-otservbr-global/migrations/22.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 23 (fix offline training skill size)") + logger.info("Updating database to version 22 (fix offline training skill size)") db.query([[ ALTER TABLE `players` MODIFY offlinetraining_skill tinyint(2) NOT NULL DEFAULT '-1'; diff --git a/data-otservbr-global/migrations/23.lua b/data-otservbr-global/migrations/23.lua index 8edac8cef47..dbf161bb474 100644 --- a/data-otservbr-global/migrations/23.lua +++ b/data-otservbr-global/migrations/23.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 24 (forge history)") + logger.info("Updating database to version 23 (forge history)") db.query([[ CREATE TABLE IF NOT EXISTS `forge_history` ( `id` int NOT NULL AUTO_INCREMENT, diff --git a/data-otservbr-global/migrations/24.lua b/data-otservbr-global/migrations/24.lua index fed9f189085..2d5286e5608 100644 --- a/data-otservbr-global/migrations/24.lua +++ b/data-otservbr-global/migrations/24.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 25 (random mount outfit window)") + logger.info("Updating database to version 24 (random mount outfit window)") db.query("ALTER TABLE `players` ADD `randomize_mount` SMALLINT(1) NOT NULL DEFAULT '0'") end diff --git a/data-otservbr-global/migrations/25.lua b/data-otservbr-global/migrations/25.lua index 4d229bb58e5..41f83e2b72b 100644 --- a/data-otservbr-global/migrations/25.lua +++ b/data-otservbr-global/migrations/25.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 26 (reward bag fix)") + logger.info("Updating database to version 25 (reward bag fix)") db.query("UPDATE player_rewards SET pid = 0 WHERE itemtype = 19202;") end diff --git a/data-otservbr-global/migrations/26.lua b/data-otservbr-global/migrations/26.lua index ddf821ca5cd..24db966339f 100644 --- a/data-otservbr-global/migrations/26.lua +++ b/data-otservbr-global/migrations/26.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 27 (towns)") + logger.info("Updating database to version 26 (towns)") db.query([[ CREATE TABLE IF NOT EXISTS `towns` ( diff --git a/data-otservbr-global/migrations/27.lua b/data-otservbr-global/migrations/27.lua index 73e7bf5c4f7..478e6da6207 100644 --- a/data-otservbr-global/migrations/27.lua +++ b/data-otservbr-global/migrations/27.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 28 (bosstiary system)") + logger.info("Updating database to version 27 (bosstiary system)") db.query("ALTER TABLE `players` ADD `boss_points` int NOT NULL DEFAULT '0';") db.query([[ CREATE TABLE IF NOT EXISTS `boosted_boss` ( diff --git a/data-otservbr-global/migrations/28.lua b/data-otservbr-global/migrations/28.lua index d7575edf3c8..06adece4d49 100644 --- a/data-otservbr-global/migrations/28.lua +++ b/data-otservbr-global/migrations/28.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 29 (transfer coins)") + logger.info("Updating database to version 28 (transfer coins)") db.query("ALTER TABLE `accounts` ADD `coins_transferable` int unsigned NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/29.lua b/data-otservbr-global/migrations/29.lua index 0c633e46b5d..91834f4f2f6 100644 --- a/data-otservbr-global/migrations/29.lua +++ b/data-otservbr-global/migrations/29.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 30 (looktypeEx)") + logger.info("Updating database to version 29 (looktypeEx)") db.query("ALTER TABLE `boosted_boss` ADD `looktypeEx` int unsigned NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/3.lua b/data-otservbr-global/migrations/3.lua index ae06343be07..0a1aec6f6df 100644 --- a/data-otservbr-global/migrations/3.lua +++ b/data-otservbr-global/migrations/3.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 4 (prey tick)") + logger.info("Updating database to version 3 (prey tick)") db.query([[ ALTER TABLE `prey_slots` diff --git a/data-otservbr-global/migrations/30.lua b/data-otservbr-global/migrations/30.lua index 4ee1632421d..4749f1588ac 100644 --- a/data-otservbr-global/migrations/30.lua +++ b/data-otservbr-global/migrations/30.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 31 (loyalty)") + logger.info("Updating database to version 30 (loyalty)") db.query([[ ALTER TABLE `accounts` ADD COLUMN `premdays_purchased` int(11) NOT NULL DEFAULT 0; ]]) diff --git a/data-otservbr-global/migrations/31.lua b/data-otservbr-global/migrations/31.lua index 5ba21bbe561..9659f296ac5 100644 --- a/data-otservbr-global/migrations/31.lua +++ b/data-otservbr-global/migrations/31.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 32 (account_sessions)") + logger.info("Updating database to version 31 (account_sessions)") db.query([[ CREATE TABLE IF NOT EXISTS `account_sessions` ( `id` VARCHAR(191) NOT NULL, diff --git a/data-otservbr-global/migrations/32.lua b/data-otservbr-global/migrations/32.lua index 078ef407da6..c90de61886b 100644 --- a/data-otservbr-global/migrations/32.lua +++ b/data-otservbr-global/migrations/32.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 33 (wheel of destiny)") + logger.info("Updating database to version 32 (wheel of destiny)") db.query([[ CREATE TABLE IF NOT EXISTS `player_wheeldata` ( `player_id` int(11) NOT NULL, diff --git a/data-otservbr-global/migrations/33.lua b/data-otservbr-global/migrations/33.lua index 2c77cbb6e24..7c0852a32bc 100644 --- a/data-otservbr-global/migrations/33.lua +++ b/data-otservbr-global/migrations/33.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 34 (add primary keys)") + logger.info("Updating database to version 33 (add primary keys)") db.query([[ ALTER TABLE `player_prey` ADD PRIMARY KEY (`player_id`, `slot`); diff --git a/data-otservbr-global/migrations/34.lua b/data-otservbr-global/migrations/34.lua index 7537f6e6582..c344eae6b8d 100644 --- a/data-otservbr-global/migrations/34.lua +++ b/data-otservbr-global/migrations/34.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 35 (bosstiary tracker)") + logger.info("Updating database to version 34 (bosstiary tracker)") db.query("ALTER TABLE `player_bosstiary` ADD `tracker` blob NOT NULL;") end diff --git a/data-otservbr-global/migrations/35.lua b/data-otservbr-global/migrations/35.lua index 70c820c32fd..9e2ab4dd1ba 100644 --- a/data-otservbr-global/migrations/35.lua +++ b/data-otservbr-global/migrations/35.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 36 (fix account premdays and lastday)") + logger.info("Updating database to version 35 (fix account premdays and lastday)") local resultQuery = db.storeQuery("SELECT `id`, `premdays`, `lastday` FROM `accounts` WHERE (`premdays` > 0 OR `lastday` > 0) AND `lastday` <= " .. os.time()) if resultQuery ~= false then diff --git a/data-otservbr-global/migrations/36.lua b/data-otservbr-global/migrations/36.lua index 5f912763cc8..5fab551e4c0 100644 --- a/data-otservbr-global/migrations/36.lua +++ b/data-otservbr-global/migrations/36.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 37 (add coin_type to accounts)") + logger.info("Updating database to version 36 (add coin_type to accounts)") db.query("ALTER TABLE `coins_transactions` ADD `coin_type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1';") end diff --git a/data-otservbr-global/migrations/37.lua b/data-otservbr-global/migrations/37.lua index ae3dbefbc27..7219dfbbac2 100644 --- a/data-otservbr-global/migrations/37.lua +++ b/data-otservbr-global/migrations/37.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 38 (add pronoun to players)") + logger.info("Updating database to version 37 (add pronoun to players)") db.query("ALTER TABLE `players` ADD `pronoun` int(11) NOT NULL DEFAULT '0';") end diff --git a/data-otservbr-global/migrations/38.lua b/data-otservbr-global/migrations/38.lua index 3412e4d488f..7e9e3748175 100644 --- a/data-otservbr-global/migrations/38.lua +++ b/data-otservbr-global/migrations/38.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 39 (create kv store)") + logger.info("Updating database to version 38 (create kv store)") db.query([[ CREATE TABLE IF NOT EXISTS `kv_store` ( `key_name` varchar(191) NOT NULL, diff --git a/data-otservbr-global/migrations/39.lua b/data-otservbr-global/migrations/39.lua index 181994882db..f660e98eb37 100644 --- a/data-otservbr-global/migrations/39.lua +++ b/data-otservbr-global/migrations/39.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 40 (house transfer ownership on startup)") + logger.info("Updating database to version 39 (house transfer ownership on startup)") db.query("ALTER TABLE `houses` ADD `new_owner` int(11) NOT NULL DEFAULT '-1';") end diff --git a/data-otservbr-global/migrations/4.lua b/data-otservbr-global/migrations/4.lua index 891bc20915a..a7b04453374 100644 --- a/data-otservbr-global/migrations/4.lua +++ b/data-otservbr-global/migrations/4.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 5 (boosted creature)") + logger.info("Updating database to version 4 (boosted creature)") db.query([[CREATE TABLE IF NOT EXISTS `boosted_creature` ( `boostname` TEXT, `date` varchar(250) NOT NULL DEFAULT '', diff --git a/data-otservbr-global/migrations/40.lua b/data-otservbr-global/migrations/40.lua index be8794e7b25..a7d3ae6afc2 100644 --- a/data-otservbr-global/migrations/40.lua +++ b/data-otservbr-global/migrations/40.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 41 (optimize house_lists)") + logger.info("Updating database to version 40 (optimize house_lists)") db.query([[ ALTER TABLE `house_lists` diff --git a/data-otservbr-global/migrations/41.lua b/data-otservbr-global/migrations/41.lua index 1fa9a40e36d..504bd950073 100644 --- a/data-otservbr-global/migrations/41.lua +++ b/data-otservbr-global/migrations/41.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 42 (fix xpboost types)") + logger.info("Updating database to version 41 (fix xpboost types)") db.query([[ ALTER TABLE `players` diff --git a/data-otservbr-global/migrations/42.lua b/data-otservbr-global/migrations/42.lua index 4b0b97b9987..6bc750efa66 100644 --- a/data-otservbr-global/migrations/42.lua +++ b/data-otservbr-global/migrations/42.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 43 (fix guildwar_kills_unique)") + logger.info("Updating database to version 42 (fix guildwar_kills_unique)") db.query([[ ALTER TABLE `guildwar_kills` diff --git a/data-otservbr-global/migrations/43.lua b/data-otservbr-global/migrations/43.lua index 438ba91b713..bcf1658864d 100644 --- a/data-otservbr-global/migrations/43.lua +++ b/data-otservbr-global/migrations/43.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 44 (feat frags_limit, payment and duration_days in guild wars)") + logger.info("Updating database to version 43 (feat frags_limit, payment and duration_days in guild wars)") db.query([[ ALTER TABLE `guild_wars` diff --git a/data-otservbr-global/migrations/44.lua b/data-otservbr-global/migrations/44.lua index 0e2140c3183..acef11ceed9 100644 --- a/data-otservbr-global/migrations/44.lua +++ b/data-otservbr-global/migrations/44.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 45 (fix: mana shield column size for more than 65k)") + logger.info("Updating database to version 44 (fix: mana shield column size for more than 65k)") db.query([[ ALTER TABLE `players` diff --git a/data-otservbr-global/migrations/45.lua b/data-otservbr-global/migrations/45.lua index a88de886861..abed3464072 100644 --- a/data-otservbr-global/migrations/45.lua +++ b/data-otservbr-global/migrations/45.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 46 (feat: vip groups)") + logger.info("Updating database to version 45 (feat: vip groups)") db.query([[ CREATE TABLE IF NOT EXISTS `account_vipgroups` ( diff --git a/data-otservbr-global/migrations/46.lua b/data-otservbr-global/migrations/46.lua index da4ade8cbf3..d7f24765b8a 100644 --- a/data-otservbr-global/migrations/46.lua +++ b/data-otservbr-global/migrations/46.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 47 (fix: creature speed and conditions)") + logger.info("Updating database to version 46 (fix: creature speed and conditions)") db.query("ALTER TABLE `players` MODIFY `conditions` mediumblob NOT NULL;") end diff --git a/data-otservbr-global/migrations/47.lua b/data-otservbr-global/migrations/47.lua index 3c8908b5641..6b658e4085f 100644 --- a/data-otservbr-global/migrations/47.lua +++ b/data-otservbr-global/migrations/47.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 46 (hireling)") + logger.info("Updating database to version 47 (hireling)") db.query([[ CREATE TABLE IF NOT EXISTS `player_hirelings` ( diff --git a/data-otservbr-global/migrations/48.lua b/data-otservbr-global/migrations/48.lua new file mode 100644 index 00000000000..53d6ba3a948 --- /dev/null +++ b/data-otservbr-global/migrations/48.lua @@ -0,0 +1,27 @@ +function onUpdateDatabase() + logger.info("Updating database to version 48 (House Auction)") + + db.query([[ + ALTER TABLE `houses` + DROP `bid`, + DROP `bid_end`, + DROP `last_bid`, + DROP `highest_bidder` + ]]) + + db.query([[ + ALTER TABLE `houses` + ADD `bidder` int(11) NOT NULL DEFAULT '0', + ADD `bidder_name` varchar(255) NOT NULL DEFAULT '', + ADD `highest_bid` int(11) NOT NULL DEFAULT '0', + ADD `internal_bid` int(11) NOT NULL DEFAULT '0', + ADD `bid_end_date` int(11) NOT NULL DEFAULT '0', + ADD `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + ADD `transfer_status` tinyint(1) DEFAULT '0' + ]]) + + db.query([[ + ALTER TABLE `accounts` + ADD `house_bid_id` int(11) NOT NULL DEFAULT '0' + ]]) +end diff --git a/data-otservbr-global/migrations/5.lua b/data-otservbr-global/migrations/5.lua index dbc324198dd..4b027c20085 100644 --- a/data-otservbr-global/migrations/5.lua +++ b/data-otservbr-global/migrations/5.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 6 (quickloot)") + logger.info("Updating database to version 5 (quickloot)") db.query("ALTER TABLE `players` ADD `quickloot_fallback` TINYINT DEFAULT 0") end diff --git a/data-otservbr-global/migrations/6.lua b/data-otservbr-global/migrations/6.lua index 91766a68ca7..cc3a3f76423 100644 --- a/data-otservbr-global/migrations/6.lua +++ b/data-otservbr-global/migrations/6.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 7 (Stash supply)") + logger.info("Updating database to version 6 (Stash supply)") db.query([[CREATE TABLE IF NOT EXISTS `player_stash` ( `player_id` INT(16) NOT NULL, `item_id` INT(16) NOT NULL, diff --git a/data-otservbr-global/migrations/7.lua b/data-otservbr-global/migrations/7.lua index cade1faae1f..0666d7cf9d8 100644 --- a/data-otservbr-global/migrations/7.lua +++ b/data-otservbr-global/migrations/7.lua @@ -1,4 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 8 (recruiter system)") + logger.info("Updating database to version 7 (recruiter system)") db.query("ALTER TABLE `accounts` ADD `recruiter` INT(6) DEFAULT 0") end diff --git a/data-otservbr-global/migrations/8.lua b/data-otservbr-global/migrations/8.lua index 523b705c0c7..a96db82e278 100644 --- a/data-otservbr-global/migrations/8.lua +++ b/data-otservbr-global/migrations/8.lua @@ -1,29 +1,30 @@ function onUpdateDatabase() - logger.info("Updating database to version 9 (Bestiary cpp)") + logger.info("Updating database to version 8 (Bestiary cpp)") db.query([[CREATE TABLE IF NOT EXISTS `player_charms` ( -`player_guid` INT(250) NOT NULL , -`charm_points` VARCHAR(250) NULL , -`charm_expansion` BOOLEAN NULL , -`rune_wound` INT(250) NULL , -`rune_enflame` INT(250) NULL , -`rune_poison` INT(250) NULL , -`rune_freeze` INT(250) NULL , -`rune_zap` INT(250) NULL , -`rune_curse` INT(250) NULL , -`rune_cripple` INT(250) NULL , -`rune_parry` INT(250) NULL , -`rune_dodge` INT(250) NULL , -`rune_adrenaline` INT(250) NULL , -`rune_numb` INT(250) NULL, -`rune_cleanse` INT(250) NULL , -`rune_bless` INT(250) NULL , -`rune_scavenge` INT(250) NULL , -`rune_gut` INT(250) NULL , -`rune_low_blow` INT(250) NULL , -`rune_divine` INT(250) NULL , -`rune_vamp` INT(250) NULL , -`rune_void` INT(250) NULL , -`UsedRunesBit` VARCHAR(250) NULL , -`UnlockedRunesBit` VARCHAR(250) NULL, -`tracker list` BLOB NULL ) ENGINE = InnoDB DEFAULT CHARSET=utf8;]]) + `player_guid` INT(250) NOT NULL , + `charm_points` VARCHAR(250) NULL , + `charm_expansion` BOOLEAN NULL , + `rune_wound` INT(250) NULL , + `rune_enflame` INT(250) NULL , + `rune_poison` INT(250) NULL , + `rune_freeze` INT(250) NULL , + `rune_zap` INT(250) NULL , + `rune_curse` INT(250) NULL , + `rune_cripple` INT(250) NULL , + `rune_parry` INT(250) NULL , + `rune_dodge` INT(250) NULL , + `rune_adrenaline` INT(250) NULL , + `rune_numb` INT(250) NULL, + `rune_cleanse` INT(250) NULL , + `rune_bless` INT(250) NULL , + `rune_scavenge` INT(250) NULL , + `rune_gut` INT(250) NULL , + `rune_low_blow` INT(250) NULL , + `rune_divine` INT(250) NULL , + `rune_vamp` INT(250) NULL , + `rune_void` INT(250) NULL , + `UsedRunesBit` VARCHAR(250) NULL , + `UnlockedRunesBit` VARCHAR(250) NULL, + `tracker list` BLOB NULL ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + ]]) end diff --git a/data-otservbr-global/migrations/9.lua b/data-otservbr-global/migrations/9.lua index 7ce8e189768..23516833fbb 100644 --- a/data-otservbr-global/migrations/9.lua +++ b/data-otservbr-global/migrations/9.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 10 (Mount Colors and familiars)") + logger.info("Updating database to version 9 (Mount Colors and familiars)") db.query("ALTER TABLE `players` ADD `lookmountbody` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmountfeet` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmounthead` tinyint(3) unsigned NOT NULL DEFAULT '0'") diff --git a/data-otservbr-global/world/otservbr-house.xml b/data-otservbr-global/world/otservbr-house.xml index 7eff23b4606..bedef70ff1f 100644 --- a/data-otservbr-global/world/otservbr-house.xml +++ b/data-otservbr-global/world/otservbr-house.xml @@ -1,987 +1,987 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/scripts/globalevents/server_initialization.lua b/data/scripts/globalevents/server_initialization.lua index df29660d373..a58cf01d3a2 100644 --- a/data/scripts/globalevents/server_initialization.lua +++ b/data/scripts/globalevents/server_initialization.lua @@ -27,29 +27,6 @@ local function moveExpiredBansToHistory() end end --- Function to check and process house auctions -local function processHouseAuctions() - local resultId = db.storeQuery("SELECT `id`, `highest_bidder`, `last_bid`, " .. "(SELECT `balance` FROM `players` WHERE `players`.`id` = `highest_bidder`) AS `balance` " .. "FROM `houses` WHERE `owner` = 0 AND `bid_end` != 0 AND `bid_end` < " .. os.time()) - if resultId then - repeat - local house = House(Result.getNumber(resultId, "id")) - if house then - local highestBidder = Result.getNumber(resultId, "highest_bidder") - local balance = Result.getNumber(resultId, "balance") - local lastBid = Result.getNumber(resultId, "last_bid") - if balance >= lastBid then - db.query("UPDATE `players` SET `balance` = " .. (balance - lastBid) .. " WHERE `id` = " .. highestBidder) - house:setHouseOwner(highestBidder) - end - - db.asyncQuery("UPDATE `houses` SET `last_bid` = 0, `bid_end` = 0, `highest_bidder` = 0, `bid` = 0 " .. "WHERE `id` = " .. house:getId()) - end - until not Result.next(resultId) - - Result.free(resultId) - end -end - -- Function to store towns in the database local function storeTownsInDatabase() db.query("TRUNCATE TABLE `towns`") @@ -150,7 +127,6 @@ function serverInitialization.onStartup() cleanupDatabase() moveExpiredBansToHistory() - processHouseAuctions() storeTownsInDatabase() checkAndLogDuplicateValues({ "Global", "GlobalStorage", "Storage" }) updateEventRates() diff --git a/data/scripts/talkactions/player/buy_house.lua b/data/scripts/talkactions/player/buy_house.lua index c3784d81a6b..84d3a34aad0 100644 --- a/data/scripts/talkactions/player/buy_house.lua +++ b/data/scripts/talkactions/player/buy_house.lua @@ -60,6 +60,8 @@ function buyHouse.onSay(player, words, param) return true end -buyHouse:separator(" ") -buyHouse:groupType("normal") -buyHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + buyHouse:separator(" ") + buyHouse:groupType("normal") + buyHouse:register() +end diff --git a/data/scripts/talkactions/player/leave_house.lua b/data/scripts/talkactions/player/leave_house.lua index 20ad186f2d2..d954eb1dcf0 100644 --- a/data/scripts/talkactions/player/leave_house.lua +++ b/data/scripts/talkactions/player/leave_house.lua @@ -42,6 +42,8 @@ function leaveHouse.onSay(player, words, param) return true end -leaveHouse:separator(" ") -leaveHouse:groupType("normal") -leaveHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + leaveHouse:separator(" ") + leaveHouse:groupType("normal") + leaveHouse:register() +end diff --git a/data/scripts/talkactions/player/sell_house.lua b/data/scripts/talkactions/player/sell_house.lua index c96cb5f71c3..dadadd066d1 100644 --- a/data/scripts/talkactions/player/sell_house.lua +++ b/data/scripts/talkactions/player/sell_house.lua @@ -20,6 +20,8 @@ function sellHouse.onSay(player, words, param) return true end -sellHouse:separator(" ") -sellHouse:groupType("normal") -sellHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + sellHouse:separator(" ") + sellHouse:groupType("normal") + sellHouse:register() +end diff --git a/schema.sql b/schema.sql index 86ea9e1bf6b..6fe1f21cbb8 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '46'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '48'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `tournament_coins` int(12) UNSIGNED NOT NULL DEFAULT '0', `creation` int(11) UNSIGNED NOT NULL DEFAULT '0', `recruiter` INT(6) DEFAULT 0, + `house_bid_id` int(11) NOT NULL DEFAULT '0', CONSTRAINT `accounts_pk` PRIMARY KEY (`id`), CONSTRAINT `accounts_unique` UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -451,13 +452,16 @@ CREATE TABLE IF NOT EXISTS `houses` ( `name` varchar(255) NOT NULL, `rent` int(11) NOT NULL DEFAULT '0', `town_id` int(11) NOT NULL DEFAULT '0', - `bid` int(11) NOT NULL DEFAULT '0', - `bid_end` int(11) NOT NULL DEFAULT '0', - `last_bid` int(11) NOT NULL DEFAULT '0', - `highest_bidder` int(11) NOT NULL DEFAULT '0', `size` int(11) NOT NULL DEFAULT '0', `guildid` int(11), `beds` int(11) NOT NULL DEFAULT '0', + `bidder` int(11) NOT NULL DEFAULT '0', + `bidder_name` varchar(255) NOT NULL DEFAULT '', + `highest_bid` int(11) NOT NULL DEFAULT '0', + `internal_bid` int(11) NOT NULL DEFAULT '0', + `bid_end_date` int(11) NOT NULL DEFAULT '0', + `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + `transfer_status` tinyint(1) DEFAULT '0', INDEX `owner` (`owner`), INDEX `town_id` (`town_id`), CONSTRAINT `houses_pk` PRIMARY KEY (`id`) diff --git a/src/account/account.cpp b/src/account/account.cpp index 93596f77b15..db79f942527 100644 --- a/src/account/account.cpp +++ b/src/account/account.cpp @@ -300,3 +300,10 @@ uint32_t Account::getAccountAgeInDays() const { [[nodiscard]] time_t Account::getPremiumLastDay() const { return m_account->premiumLastDay; } + +uint32_t Account::getHouseBidId() const { + return m_account->houseBidId; +} +void Account::setHouseBidId(uint32_t houseId) { + m_account->houseBidId = houseId; +} diff --git a/src/account/account.hpp b/src/account/account.hpp index 2c6098a8dbd..0a2bcc1a2b2 100644 --- a/src/account/account.hpp +++ b/src/account/account.hpp @@ -119,6 +119,9 @@ class Account { std::tuple, AccountErrors_t> getAccountPlayers() const; + void setHouseBidId(uint32_t houseId); + uint32_t getHouseBidId() const; + // Old protocol compat void setProtocolCompat(bool toggle); diff --git a/src/account/account_info.hpp b/src/account/account_info.hpp index b9dad60dbbc..54741419ddb 100644 --- a/src/account/account_info.hpp +++ b/src/account/account_info.hpp @@ -28,4 +28,5 @@ struct AccountInfo { time_t sessionExpires = 0; uint32_t premiumDaysPurchased = 0; uint32_t creationTime = 0; + uint32_t houseBidId = 0; }; diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index c3f02bfe972..b2e8fd80754 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -47,12 +47,13 @@ bool AccountRepositoryDB::loadBySession(const std::string &sessionKey, std::uniq bool AccountRepositoryDB::save(const std::unique_ptr &accInfo) { bool successful = g_database().executeQuery( fmt::format( - "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {} WHERE `id` = {}", + "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {}, `house_bid_id` = {} WHERE `id` = {}", accInfo->accountType, accInfo->premiumRemainingDays, accInfo->premiumLastDay, accInfo->creationTime, accInfo->premiumDaysPurchased, + accInfo->houseBidId, accInfo->id ) ); diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 559045fdb9b..65e585159e8 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -46,6 +46,7 @@ enum ConfigKey_t : uint16_t { CONVERT_UNSAFE_SCRIPTS, CORE_DIRECTORY, CRITICALCHANCE, + CYCLOPEDIA_HOUSE_AUCTION, DATA_DIRECTORY, DAY_KILLS_TO_RED, DEATH_LOSE_PERCENT, @@ -110,6 +111,7 @@ enum ConfigKey_t : uint16_t { HAZARD_PODS_TIME_TO_DAMAGE, HAZARD_PODS_TIME_TO_SPAWN, HAZARD_SPAWN_PLUNDER_MULTIPLIER, + DAYS_TO_CLOSE_BID, HOUSE_BUY_LEVEL, HOUSE_LOSE_AFTER_INACTIVITY, HOUSE_OWNED_BY_ACCOUNT, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1c00df76c2f..1034a28be3e 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -157,6 +157,7 @@ bool ConfigManager::load() { loadBoolConfig(L, VIP_SYSTEM_ENABLED, "vipSystemEnabled", false); loadBoolConfig(L, WARN_UNSAFE_SCRIPTS, "warnUnsafeScripts", true); loadBoolConfig(L, XP_DISPLAY_MODE, "experienceDisplayRates", true); + loadBoolConfig(L, CYCLOPEDIA_HOUSE_AUCTION, "toggleCyclopediaHouseAuction", true); loadFloatConfig(L, BESTIARY_RATE_CHARM_SHOP_PRICE, "bestiaryRateCharmShopPrice", 1.0); loadFloatConfig(L, COMBAT_CHAIN_SKILL_FORMULA_AXE, "combatChainSkillFormulaAxe", 0.9); @@ -255,6 +256,7 @@ bool ConfigManager::load() { loadIntConfig(L, HAZARD_PODS_TIME_TO_DAMAGE, "hazardPodsTimeToDamage", 2000); loadIntConfig(L, HAZARD_PODS_TIME_TO_SPAWN, "hazardPodsTimeToSpawn", 4000); loadIntConfig(L, HAZARD_SPAWN_PLUNDER_MULTIPLIER, "hazardSpawnPlunderMultiplier", 25); + loadIntConfig(L, DAYS_TO_CLOSE_BID, "daysToCloseBid", 7); loadIntConfig(L, HOUSE_BUY_LEVEL, "houseBuyLevel", 0); loadIntConfig(L, HOUSE_LOSE_AFTER_INACTIVITY, "houseLoseAfterInactivity", 0); loadIntConfig(L, HOUSE_PRICE_PER_SQM, "housePriceEachSQM", 1000); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 88223534497..bfbc8148d0a 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -37,6 +37,7 @@ #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" #include "enums/player_icons.hpp" +#include "enums/player_cyclopedia.hpp" #include "game/game.hpp" #include "game/modal_window/modal_window.hpp" #include "game/scheduling/dispatcher.hpp" @@ -2264,6 +2265,22 @@ void Player::sendOutfitWindow() const { } } +void Player::sendCyclopediaHouseList(const HouseMap &houses) const { + if (client) { + client->sendCyclopediaHouseList(houses); + } +} +void Player::sendResourceBalance(Resource_t resourceType, uint64_t value) const { + if (client) { + client->sendResourceBalance(resourceType, value); + } +} +void Player::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) const { + if (client) { + client->sendHouseAuctionMessage(houseId, type, index, bidSuccess); + } +} + // Imbuements void Player::onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm) { @@ -10447,3 +10464,108 @@ uint16_t Player::getPlayerVocationEnum() const { return Vocation_t::VOCATION_NONE; } + +BidErrorMessage Player::canBidHouse(uint32_t houseId) { + using enum BidErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + if (getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughMoney; + } + + if (house->isGuildhall()) { + if (getGuildRank() && getGuildRank()->level != 3) { + return Guildhall; + } + + if (getGuild() && getGuild()->getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughGuildMoney; + } + } + + return NoError; +} + +TransferErrorMessage Player::canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID) { + using enum TransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getOwner()) { + return NotHouseOwner; + } + + if (getGUID() == newOwnerGUID) { + return AlreadyTheOwner; + } + + const auto newOwner = g_game().getPlayerByGUID(newOwnerGUID, true); + if (!newOwner) { + return CharacterNotExist; + } + + if (newOwner->getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!newOwner->isPremium()) { + return Premium; + } + + if (newOwner->getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + return Success; +} + +AcceptTransferErrorMessage Player::canAcceptTransferHouse(uint32_t houseId) { + using enum AcceptTransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getBidder()) { + return NotNewOwner; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return AlreadyBid; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (getBankBalance() < (house->getRent() + house->getInternalBid())) { + return Frozen; + } + + if (house->getTransferStatus()) { + return AlreadyAccepted; + } + + return Success; +} diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 6ab3be4ca7a..1b52607f2d8 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -64,17 +64,23 @@ struct HighscoreCharacter; enum class PlayerIcon : uint8_t; enum class IconBakragore : uint8_t; +enum class HouseAuctionType : uint8_t; +enum class BidErrorMessage : uint8_t; +enum class TransferErrorMessage : uint8_t; +enum class AcceptTransferErrorMessage : uint8_t; enum ObjectCategory_t : uint8_t; enum PreySlot_t : uint8_t; enum SpeakClasses : uint8_t; enum ChannelEvent_t : uint8_t; enum SquareColor_t : uint8_t; +enum Resource_t : uint8_t; using GuildWarVector = std::vector; using StashContainerList = std::vector, uint32_t>>; using ItemVector = std::vector>; using UsersMap = std::map>; using InvitedMap = std::map>; +using HouseMap = std::map>; struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; @@ -880,6 +886,13 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendOpenPrivateChannel(const std::string &receiver) const; void sendExperienceTracker(int64_t rawExp, int64_t finalExp) const; void sendOutfitWindow() const; + // House Auction + BidErrorMessage canBidHouse(uint32_t houseId); + TransferErrorMessage canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID); + AcceptTransferErrorMessage canAcceptTransferHouse(uint32_t houseId); + void sendCyclopediaHouseList(const HouseMap &houses) const; + void sendResourceBalance(Resource_t resourceType, uint64_t value) const; + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess = false) const; // Imbuements void onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm); void onClearImbuement(const std::shared_ptr &item, uint8_t slot); diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp index af7ea1701ff..4d2227f8d48 100644 --- a/src/enums/player_cyclopedia.hpp +++ b/src/enums/player_cyclopedia.hpp @@ -60,3 +60,61 @@ enum class CyclopediaMapData_t : uint8_t { Donations = 9, SetCurrentArea = 10, }; + +enum class CyclopediaHouseState : uint8_t { + Available = 0, + Rented = 2, + Transfer = 3, + MoveOut = 4, +}; + +enum class HouseAuctionType : uint8_t { + Bid = 1, + MoveOut = 2, + Transfer = 3, + CancelMoveOut = 4, + CancelTransfer = 5, + AcceptTransfer = 6, + RejectTransfer = 7, +}; + +enum class BidSuccessMessage : uint8_t { + BidSuccess = 0, + LowerBid = 1, +}; + +enum class BidErrorMessage : uint8_t { + NoError = 0, + Rookgaard = 3, + Premium = 5, + Guildhall = 6, + OnlyOneBid = 7, + NotEnoughMoney = 17, + NotEnoughGuildMoney = 21, + Internal = 24, +}; + +// Bytes to: +// Move Out, Transfer +// Cancel Move Out/Transfer +enum class TransferErrorMessage : uint8_t { + Success = 0, + NotHouseOwner = 2, + CharacterNotExist = 4, + Premium = 7, + Rookgaard = 16, + AlreadyTheOwner = 19, + OnlyOneBid = 25, + Internal = 32, +}; + +enum class AcceptTransferErrorMessage : uint8_t { + Success = 0, + NotNewOwner = 2, + AlreadyBid = 3, + AlreadyAccepted = 7, + Rookgaard = 8, + Premium = 9, + Frozen = 15, + Internal = 19, +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index 5c7dc7aaf16..e3414f74725 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -10864,3 +10864,354 @@ void Game::updatePlayersOnline() const { g_logger().error("[Game::updatePlayersOnline] Failed to update players online."); } } + +void Game::playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName) { + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + HouseMap houses; + if (!townName.empty()) { + const auto &housesList = g_game().map.houses.getHouses(); + for (const auto &it : housesList) { + const auto &house = it.second; + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string &houseTown = town->getName(); + if (houseTown == townName) { + houses.emplace(house->getClientId(), house); + } + } + } else { + auto playerHouses = g_game().map.houses.getAllHousesByPlayerId(player->getGUID()); + if (playerHouses.size()) { + for (const auto &playerHouse : playerHouses) { + if (!playerHouse) { + continue; + } + houses.emplace(playerHouse->getClientId(), playerHouse); + } + } + + const auto house = g_game().map.houses.getHouseByBidderName(player->getName()); + if (house) { + houses.emplace(house->getClientId(), house); + } + } + player->sendCyclopediaHouseList(houses); +} + +void Game::playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return; + } + + auto ret = player->canBidHouse(houseId); + if (ret != BidErrorMessage::NoError) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + } + ret = BidErrorMessage::NotEnoughMoney; + auto retSuccess = BidSuccessMessage::BidSuccess; + + if (house->getBidderName().empty()) { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(0); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + house->calculateBidEndDate(g_configManager().getNumber(DAYS_TO_CLOSE_BID)); + } else if (house->getBidderName() == player->getName()) { + if (!processBankAuction(player, house, bidValue, true)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + } else if (bidValue <= house->getInternalBid()) { + house->setHighestBid(bidValue); + retSuccess = BidSuccessMessage::LowerBid; + } else { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(house->getInternalBid() + 1); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + } + + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string houseTown = town->getName(); + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(retSuccess), true); + playerCyclopediaHousesByTown(playerId, houseTown); +} + +void Game::playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::MoveOut); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::MoveOut) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &owner = getPlayerByID(playerId); + if (!owner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const std::shared_ptr &newOwner = getPlayerByName(newOwnerName, true); + if (!newOwner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::CharacterNotExist)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + auto ret = owner->canTransferHouse(houseId, newOwner->getGUID()); + if (ret != TransferErrorMessage::Success) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + return; + } + + house->setBidderName(newOwnerName); + house->setBidder(newOwner->getGUID()); + house->setInternalBid(bidValue); + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::Transfer); + + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + auto ret = player->canAcceptTransferHouse(houseId); + if (ret != AcceptTransferErrorMessage::Success) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + return; + } + + if (!processBankAuction(player, house, house->getInternalBid())) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Frozen)); + return; + } + + house->setTransferStatus(true); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getBidder() != player->getGUID() || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::RejectTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +bool Game::processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace /* = false*/) { + if (!replace && player->getBankBalance() < (house->getRent() + bid)) { + return false; + } + + if (player->getBankBalance() < bid) { + return false; + } + + uint64_t balance = player->getBankBalance(); + if (replace) { + player->setBankBalance(balance - (bid - house->getInternalBid())); + } else { + player->setBankBalance(balance - (house->getRent() + bid)); + } + + player->sendResourceBalance(RESOURCE_BANK, player->getBankBalance()); + + if (house->getBidderName() != player->getName()) { + const auto otherPlayer = g_game().getPlayerByName(house->getBidderName()); + if (!otherPlayer) { + uint32_t bidderGuid = IOLoginData::getGuidByName(house->getBidderName()); + IOLoginData::increaseBankBalance(bidderGuid, (house->getBidHolderLimit() + house->getRent())); + } else { + otherPlayer->setBankBalance(otherPlayer->getBankBalance() + (house->getBidHolderLimit() + house->getRent())); + otherPlayer->sendResourceBalance(RESOURCE_BANK, otherPlayer->getBankBalance()); + } + } + + return true; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index ff2e127fd25..b16d1787d69 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -289,6 +289,17 @@ class Game { void playerHighscores(const std::shared_ptr &player, HighscoreType_t type, uint8_t category, uint32_t vocation, const std::string &worldName, uint16_t page, uint8_t entriesPerPage); static std::string getSkillNameById(uint8_t &skill); + // House Auction + void playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName); + void playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue); + void playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp); + void playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue); + void playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId); + bool processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace = false); + void updatePlayerSaleItems(uint32_t playerId); bool internalStartTrade(const std::shared_ptr &player, const std::shared_ptr &partner, const std::shared_ptr &tradeItem); diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index c0a6a13f363..37ec4a4dec1 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -336,14 +336,6 @@ void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) { Database::getInstance().executeQuery(query.str()); } -bool IOLoginData::hasBiddedOnHouse(uint32_t guid) { - Database &db = Database::getInstance(); - - std::ostringstream query; - query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid << " LIMIT 1"; - return db.storeQuery(query.str()).get() != nullptr; -} - std::vector IOLoginData::getVIPEntries(uint32_t accountId) { std::string query = fmt::format("SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {}", accountId); std::vector entries; diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp index d379031cd55..378e1bd2388 100644 --- a/src/io/iologindata.hpp +++ b/src/io/iologindata.hpp @@ -31,7 +31,6 @@ class IOLoginData { static std::string getNameByGuid(uint32_t guid); static bool formatPlayerName(std::string &name); static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); - static bool hasBiddedOnHouse(uint32_t guid); static std::vector getVIPEntries(uint32_t accountId); static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify); diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index b1de604dd14..1479197c2c5 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -273,7 +273,7 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, const std::shared_ptrgetNumber("id"); const auto house = g_game().map.houses.getHouse(houseId); - if (house) { - auto owner = result->getNumber("owner"); - auto newOwner = result->getNumber("new_owner"); - // Transfer house owner - auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); - if (isTransferOnRestart && newOwner >= 0) { + if (!house) { + continue; + } + + auto owner = result->getNumber("owner"); + auto newOwner = result->getNumber("new_owner"); + uint32_t bidder = result->getNumber("bidder"); + std::string bidderName = result->getString("bidder_name"); + uint32_t highestBid = result->getNumber("highest_bid"); + uint32_t internalBid = result->getNumber("internal_bid"); + uint32_t bidEndDate = result->getNumber("bid_end_date"); + auto state = static_cast(result->getNumber("state")); + auto transferStatus = result->getNumber("transfer_status"); + const auto timeNow = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + // Transfer house owner + auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); + if (isTransferOnRestart && newOwner >= 0) { + g_game().setTransferPlayerHouseItems(houseId, owner); + if (newOwner == 0) { + g_logger().debug("Removing house id '{}' owner", houseId); + house->setOwner(0); + } else { + g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); + house->setOwner(newOwner); + } + } else if (state == CyclopediaHouseState::Available && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[BID] - Setting house id '{}' owner to player GUID '{}'", houseId, bidder); + if (highestBid < internalBid) { + uint32_t diff = internalBid - highestBid; + IOLoginData::increaseBankBalance(bidder, diff); + } + house->setOwner(bidder); + bidder = 0; + bidderName = ""; + highestBid = 0; + internalBid = 0; + bidEndDate = 0; + } else if (state == CyclopediaHouseState::Transfer && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[TRANSFER] - Removing house id '{}' from owner GUID '{}' and transfering to new owner GUID '{}'", houseId, owner, bidder); + if (transferStatus) { g_game().setTransferPlayerHouseItems(houseId, owner); - if (newOwner == 0) { - g_logger().debug("Removing house id '{}' owner", houseId); - house->setOwner(0); - } else { - g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); - house->setOwner(newOwner); - } + house->setOwner(bidder); + IOLoginData::increaseBankBalance(owner, internalBid); } else { - house->setOwner(owner, false); + house->setOwner(owner); } - house->setPaidUntil(result->getNumber("paid")); - house->setPayRentWarnings(result->getNumber("warnings")); + bidder = 0; + bidderName = ""; + internalBid = 0; + bidEndDate = 0; + transferStatus = false; + } else if (state == CyclopediaHouseState::MoveOut && timeNow > bidEndDate) { + g_logger().debug("[MOVE OUT] - Removing house id '{}' owner", houseId); + g_game().setTransferPlayerHouseItems(houseId, owner); + house->setOwner(0); + bidEndDate = 0; + } else { + house->setOwner(owner, false); + house->setState(state); } + house->setBidder(bidder); + house->setBidderName(bidderName); + house->setHighestBid(highestBid); + house->setInternalBid(internalBid); + house->setBidHolderLimit(internalBid); + house->setBidEndDate(bidEndDate); + house->setTransferStatus(transferStatus); } while (result->next()); result = db.storeQuery("SELECT `house_id`, `listid`, `list` FROM `house_lists`"); @@ -331,11 +379,12 @@ bool IOMapSerialize::SaveHouseInfoGuard() { Database &db = Database::getInstance(); std::ostringstream query; - DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES "); - houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds" }); + DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`, `bidder`, `bidder_name`, `highest_bid`, `internal_bid`, `bid_end_date`, `state`, `transfer_status`) VALUES "); + houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds", "bidder", "bidder_name", "highest_bid", "internal_bid", "bid_end_date", "state", "transfer_status" }); for (const auto &[key, house] : g_game().map.houses.getHouses()) { - std::string values = fmt::format("{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount()); + auto stateValue = magic_enum::enum_integer(house->getState()); + std::string values = fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount(), house->getBidder(), db.escapeString(house->getBidderName()), house->getHighestBid(), house->getInternalBid(), house->getBidEndDate(), std::to_string(stateValue), (house->getTransferStatus() ? 1 : 0)); if (!houseUpdate.addRow(values)) { return false; diff --git a/src/lua/functions/map/house_functions.cpp b/src/lua/functions/map/house_functions.cpp index 10ebbc2cf0c..dd20d6fdcf6 100644 --- a/src/lua/functions/map/house_functions.cpp +++ b/src/lua/functions/map/house_functions.cpp @@ -9,6 +9,7 @@ #include "lua/functions/map/house_functions.hpp" +#include "account/account.hpp" #include "config/configmanager.hpp" #include "items/bed.hpp" #include "game/game.hpp" @@ -238,7 +239,7 @@ int HouseFunctions::luaHouseStartTrade(lua_State* L) { return 1; } - if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + if (tradePartner->getAccount()->getHouseBidId() != 0) { lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); return 1; } diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index 6d93561172b..b3054616bff 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -95,7 +95,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: Database &db = Database::getInstance(); std::ostringstream query; - query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; + query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `paid` = 0, `bidder` = 0, `bidder_name` = '', `highest_bid` = 0, `internal_bid` = 0, `bid_end_date` = 0, `state` = " << (guid > 0 ? 2 : 0) << " WHERE `id` = " << id; db.executeQuery(query.str()); } @@ -107,7 +107,9 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: if (owner != 0) { tryTransferOwnership(player, false); - } else { + } + + if (guid != 0) { std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); time_t currentTime = time(nullptr); if (strRentPeriod == "yearly") { @@ -123,6 +125,8 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: } paidUntil = currentTime; + } else { + paidUntil = 0; } rentWarnings = 0; @@ -141,6 +145,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: owner = guid; ownerName = name; ownerAccountId = result->getNumber("account_id"); + m_state = CyclopediaHouseState::Rented; } } @@ -155,15 +160,17 @@ void House::updateDoorDescription() const { ss << "It belongs to house '" << houseName << "'. Nobody owns this house."; } - ss << " It is " << getSize() << " square meters."; - const int32_t housePrice = getPrice(); - if (housePrice != -1) { - if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { - ss << " It costs " << formatNumber(getPrice()) << " gold coins."; - } - std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); - if (strRentPeriod != "never") { - ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + ss << " It is " << getSize() << " square meters."; + const int32_t housePrice = getPrice(); + if (housePrice != -1) { + if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { + ss << " It costs " << formatNumber(getPrice()) << " gold coins."; + } + std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); + if (strRentPeriod != "never") { + ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + } } } @@ -479,6 +486,43 @@ void House::resetTransferItem() { } } +void House::calculateBidEndDate(uint8_t daysToEnd) { + auto currentTimeMs = std::chrono::system_clock::now().time_since_epoch(); + + auto now = std::chrono::system_clock::time_point( + std::chrono::duration_cast(currentTimeMs) + ); + + // Truncate to whole days since epoch + days daysSinceEpoch = std::chrono::duration_cast(now.time_since_epoch()); + + // Get today's date at 00:00:00 UTC + auto todayMidnight = std::chrono::system_clock::time_point(daysSinceEpoch); + + std::chrono::system_clock::time_point targetDay = todayMidnight + days(daysToEnd); + + const auto serverSaveTime = g_configManager().getString(GLOBAL_SERVER_SAVE_TIME); + + std::vector params = vectorAtoi(explodeString(serverSaveTime, ":")); + int32_t hour = params.front(); + int32_t min = 0; + int32_t sec = 0; + if (params.size() > 1) { + min = params[1]; + + if (params.size() > 2) { + sec = params[2]; + } + } + std::chrono::system_clock::time_point targetTime = targetDay + std::chrono::hours(hour) + std::chrono::minutes(min) + std::chrono::seconds(sec); + + std::time_t resultTime = std::chrono::system_clock::to_time_t(targetTime); + std::tm* localTime = std::localtime(&resultTime); + auto bidEndDate = static_cast(std::mktime(localTime)); + + this->m_bidEndDate = bidEndDate; +} + std::shared_ptr HouseTransferItem::createHouseTransferItem(const std::shared_ptr &house) { auto transferItem = std::make_shared(house); transferItem->setID(ITEM_DOCUMENT_RO); @@ -725,6 +769,35 @@ std::shared_ptr Houses::getHouseByPlayerId(uint32_t playerId) const { return nullptr; } +std::vector> Houses::getAllHousesByPlayerId(uint32_t playerId) { + std::vector> playerHouses; + for (const auto &[id, house] : houseMap) { + if (house->getOwner() == playerId) { + playerHouses.emplace_back(house); + } + } + return playerHouses; +} + +std::shared_ptr Houses::getHouseByBidderName(const std::string &bidderName) { + for (const auto &[id, house] : houseMap) { + if (house->getBidderName() == bidderName) { + return house; + } + } + return nullptr; +} + +uint16_t Houses::getHouseCountByAccount(uint32_t accountId) { + uint16_t count = 0; + for (const auto &[id, house] : houseMap) { + if (house->getOwnerAccountId() == accountId) { + ++count; + } + } + return count; +} + bool Houses::loadHousesXML(const std::string &filename) { pugi::xml_document doc; const pugi::xml_parse_result result = doc.load_file(filename.c_str()); @@ -764,6 +837,13 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setRent(pugi::cast(houseNode.attribute("rent").value())); house->setSize(pugi::cast(houseNode.attribute("size").value())); house->setTownId(pugi::cast(houseNode.attribute("townid").value())); + house->setClientId(pugi::cast(houseNode.attribute("clientid").value())); + + auto guildhallAttr = houseNode.attribute("guildhall"); + if (!guildhallAttr.empty()) { + house->setGuildhall(static_cast(guildhallAttr.as_bool())); + } + auto maxBedsAttr = houseNode.attribute("beds"); int32_t maxBeds = -1; if (!maxBedsAttr.empty()) { @@ -772,6 +852,7 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setMaxBeds(maxBeds); house->setOwner(0, false); + addHouseClientId(house->getClientId(), house); } return true; } diff --git a/src/map/house/house.hpp b/src/map/house/house.hpp index a3dc765988f..d994fdbe3b2 100644 --- a/src/map/house/house.hpp +++ b/src/map/house/house.hpp @@ -13,11 +13,14 @@ #include "declarations.hpp" #include "map/house/housetile.hpp" #include "game/movement/position.hpp" +#include "enums/player_cyclopedia.hpp" class House; class BedItem; class Player; +using days = std::chrono::duration>; + class AccessList { public: void parseList(const std::string &list); @@ -233,6 +236,84 @@ class House final : public SharedObject { bool hasNewOwnership() const; void setNewOwnership(); + void setClientId(uint32_t newClientId) { + this->m_clientId = newClientId; + } + uint32_t getClientId() const { + return m_clientId; + } + + void setBidder(int32_t bidder) { + this->m_bidder = bidder; + } + int32_t getBidder() const { + return m_bidder; + } + + void setBidderName(const std::string &bidderName) { + this->m_bidderName = bidderName; + } + std::string getBidderName() const { + return m_bidderName; + } + + void setHighestBid(uint64_t bidValue) { + this->m_highestBid = bidValue; + } + uint64_t getHighestBid() const { + return m_highestBid; + } + + void setInternalBid(uint64_t bidValue) { + this->m_internalBid = bidValue; + } + uint64_t getInternalBid() const { + return m_internalBid; + } + + void setBidHolderLimit(uint64_t bidValue) { + this->m_bidHolderLimit = bidValue; + } + uint64_t getBidHolderLimit() const { + return m_bidHolderLimit; + } + + void calculateBidEndDate(uint8_t daysToEnd); + void setBidEndDate(uint32_t bidEndDate) { + this->m_bidEndDate = bidEndDate; + }; + uint32_t getBidEndDate() const { + return m_bidEndDate; + } + + void setState(CyclopediaHouseState state) { + this->m_state = state; + } + CyclopediaHouseState getState() const { + return m_state; + } + + void setTransferStatus(bool transferStatus) { + this->m_transferStatus = transferStatus; + } + bool getTransferStatus() const { + return m_transferStatus; + } + + void setOwnerAccountId(uint32_t accountId) { + this->ownerAccountId = accountId; + } + uint32_t getOwnerAccountId() const { + return ownerAccountId; + } + + void setGuildhall(bool isGuildHall) { + this->guildHall = isGuildHall; + } + bool isGuildhall() const { + return guildHall; + } + private: bool transferToDepot() const; @@ -263,9 +344,21 @@ class House final : public SharedObject { uint32_t townId = 0; uint32_t maxBeds = 4; int32_t bedsCount = -1; + bool guildHall = false; Position posEntry = {}; + // House Auction + uint32_t m_clientId; + int32_t m_bidder = 0; + std::string m_bidderName = ""; + uint64_t m_highestBid = 0; + uint64_t m_internalBid = 0; + uint64_t m_bidHolderLimit = 0; + uint32_t m_bidEndDate = 0; + CyclopediaHouseState m_state = CyclopediaHouseState::Available; + bool m_transferStatus = false; + bool isLoaded = false; void handleContainer(ItemList &moveItemList, const std::shared_ptr &item) const; @@ -299,7 +392,26 @@ class Houses { return it->second; } + void addHouseClientId(uint32_t clientId, std::shared_ptr house) { + if (auto it = houseMapClientId.find(clientId); it != houseMapClientId.end()) { + return; + } + + houseMapClientId.emplace(clientId, house); + } + + std::shared_ptr getHouseByClientId(uint32_t clientId) { + auto it = houseMapClientId.find(clientId); + if (it == houseMapClientId.end()) { + return nullptr; + } + return it->second; + } + std::shared_ptr getHouseByPlayerId(uint32_t playerId) const; + std::vector> getAllHousesByPlayerId(uint32_t playerId); + std::shared_ptr getHouseByBidderName(const std::string &bidderName); + uint16_t getHouseCountByAccount(uint32_t accountId); bool loadHousesXML(const std::string &filename); @@ -311,4 +423,5 @@ class Houses { private: HouseMap houseMap; + HouseMap houseMapClientId; }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index dc02e316f1f..60e29847477 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -55,6 +55,7 @@ #include "enums/account_type.hpp" #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" +#include "enums/player_cyclopedia.hpp" /* * NOTE: This namespace is used so that we can add functions without having to declare them in the ".hpp/.hpp" file @@ -1227,6 +1228,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xAC: parseChannelExclude(msg); break; + case 0xAD: + parseCyclopediaHouseAuction(msg); + break; case 0xAE: parseSendBosstiary(); break; @@ -6905,6 +6909,7 @@ void ProtocolGame::sendAddCreature(const std::shared_ptr &creature, co sendLootContainers(); sendBasicData(); + sendHousesInfo(); // Wheel of destiny cooldown if (!oldProtocol && g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) { player->wheel()->sendGiftOfLifeCooldown(); @@ -9341,3 +9346,192 @@ void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType) { msg.addByte(screenshotType); writeToOutputBuffer(msg); } + +void ProtocolGame::parseCyclopediaHouseAuction(NetworkMessage &msg) { + if (oldProtocol) { + return; + } + + uint8_t houseActionType = msg.getByte(); + switch (houseActionType) { + case 0: { + const auto townName = msg.getString(); + g_game().playerCyclopediaHousesByTown(player->getID(), townName); + break; + } + case 1: { + const uint32_t houseId = msg.get(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseBid(player->getID(), houseId, bidValue); + break; + } + case 2: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + g_game().playerCyclopediaHouseMoveOut(player->getID(), houseId, timestamp); + break; + } + case 3: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + const std::string &newOwner = msg.getString(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseTransfer(player->getID(), houseId, timestamp, newOwner, bidValue); + break; + } + case 4: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelMoveOut(player->getID(), houseId); + break; + } + case 5: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelTransfer(player->getID(), houseId); + break; + } + case 6: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseAcceptTransfer(player->getID(), houseId); + break; + } + case 7: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseRejectTransfer(player->getID(), houseId); + break; + } + } +} + +void ProtocolGame::sendCyclopediaHouseList(HouseMap houses) { + NetworkMessage msg; + msg.addByte(0xC7); + msg.add(houses.size()); + for (const auto &[clientId, houseData] : houses) { + msg.add(clientId); + msg.addByte(0x01); // 0x00 = Renovation; 0x01 = Available + + auto houseState = houseData->getState(); + auto stateValue = magic_enum::enum_integer(houseState); + msg.addByte(stateValue); + if (houseState == CyclopediaHouseState::Available) { + bool bidder = houseData->getBidderName() == player->getName(); + msg.addString(houseData->getBidderName()); + msg.addByte(bidder); + uint8_t disableIndex = enumToValue(player->canBidHouse(clientId)); + msg.addByte(disableIndex); + + if (!houseData->getBidderName().empty()) { + msg.add(houseData->getBidEndDate()); + msg.add(houseData->getHighestBid()); + if (bidder) { + msg.add(houseData->getBidHolderLimit()); + } + } + } else if (houseState == CyclopediaHouseState::Rented) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool rented = ownerName.compare(player->getName()) == 0; + msg.addByte(rented); + if (rented) { + msg.addByte(0); + msg.addByte(0); + } + } else if (houseState == CyclopediaHouseState::Transfer) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + } + msg.add(houseData->getBidEndDate()); + msg.addString(houseData->getBidderName()); + msg.addByte(0); // ? + msg.add(houseData->getInternalBid()); + + bool isNewOwner = player->getName() == houseData->getBidderName(); + msg.addByte(isNewOwner); + if (isNewOwner) { + uint8_t disableIndex = enumToValue(player->canAcceptTransferHouse(clientId)); + msg.addByte(disableIndex); // Accept Transfer Error + msg.addByte(0); // Reject Transfer Error + } + + if (isOwner) { + msg.addByte(0); // Cancel Transfer Error + } + } else if (houseState == CyclopediaHouseState::MoveOut) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + msg.add(houseData->getBidEndDate()); + msg.addByte(0); + } else { + msg.add(houseData->getBidEndDate()); + } + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) { + NetworkMessage msg; + const auto typeValue = enumToValue(type); + + msg.addByte(0xC3); + msg.add(houseId); + msg.addByte(typeValue); + if (bidSuccess && typeValue == 1) { + msg.addByte(0x00); + } + msg.addByte(index); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHousesInfo() { + NetworkMessage msg; + + uint32_t houseClientId = 0; + const auto accountHouseCount = g_game().map.houses.getHouseCountByAccount(player->getAccountId()); + const auto house = g_game().map.houses.getHouseByPlayerId(player->getGUID()); + if (house) { + houseClientId = house->getClientId(); + } + + msg.addByte(0xC6); + msg.add(houseClientId); + msg.addByte(0x00); + + msg.addByte(accountHouseCount); // Houses Account + + msg.addByte(0x00); + + msg.addByte(3); + msg.addByte(3); + + msg.addByte(0x01); + + msg.addByte(0x01); + msg.add(houseClientId); + + const auto &housesList = g_game().map.houses.getHouses(); + msg.add(housesList.size()); + for (const auto &it : housesList) { + msg.add(it.second->getClientId()); + } + + writeToOutputBuffer(msg); +} diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 2048bea7c16..7b7e0aacee3 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -29,6 +29,7 @@ enum Slots_t : uint8_t; enum CombatType_t : uint8_t; enum SoundEffect_t : uint16_t; enum class SourceEffect_t : uint8_t; +enum class HouseAuctionType : uint8_t; class NetworkMessage; class Player; @@ -68,6 +69,7 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ItemsTierCountList = std::map>; using StashItemList = std::map; +using HouseMap = std::map>; struct TextMessage { TextMessage() = default; @@ -353,6 +355,11 @@ class ProtocolGame final : public Protocol { void sendCyclopediaCharacterBadges(); void sendCyclopediaCharacterTitles(); + void sendHousesInfo(); + void parseCyclopediaHouseAuction(NetworkMessage &msg); + void sendCyclopediaHouseList(HouseMap houses); + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess); + void sendCreatureWalkthrough(const std::shared_ptr &creature, bool walkthrough); void sendCreatureShield(const std::shared_ptr &creature); void sendCreatureEmblem(const std::shared_ptr &creature); From 55de86ee4f475925b2d3098b2d6ac6da19e17a68 Mon Sep 17 00:00:00 2001 From: murilo09 <78226931+murilo09@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:27:34 -0300 Subject: [PATCH 21/24] fix: damage console (#3203) --- src/game/game.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index e3414f74725..8b19f8bb94f 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -7418,7 +7418,7 @@ bool Game::combatChangeHealth(const std::shared_ptr &attacker, const s } auto targetHealth = target->getHealth(); - realDamage = damage.primary.value + damage.secondary.value; + realDamage = std::min(targetHealth, damage.primary.value + damage.secondary.value); if (realDamage == 0) { return true; } else if (realDamage >= targetHealth) { From 4023def3800a8cd7523ba4c64896a9781c4a42c6 Mon Sep 17 00:00:00 2001 From: pudimtibia Date: Tue, 31 Dec 2024 13:31:56 -0300 Subject: [PATCH 22/24] fix: adjust character limit for player names (#3174) --- data/modules/scripts/gamestore/init.lua | 41 +++++++++++++++---------- src/game/game.cpp | 2 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index 433abf34492..abd125e79ef 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -1487,67 +1487,74 @@ GameStore.canChangeToName = function(name) local result = { ability = false, } - if name:len() < 3 or name:len() > 18 then - result.reason = "The length of your new name must be between 3 and 18 characters." + + if name:len() < 3 or name:len() > 29 then + result.reason = "The length of your new name must be between 3 and 29 characters." return result end local match = name:gmatch("%s+") local count = 0 - for v in match do + for _ in match do count = count + 1 end local matchtwo = name:match("^%s+") if matchtwo then - result.reason = "Your new name can't have whitespace at begin." + result.reason = "Your new name can't have whitespace at the beginning." return result end - if count > 1 then - result.reason = "Your new name have more than 1 whitespace." + if count > 2 then + result.reason = "Your new name can't have more than 2 spaces." + return result + end + + if name:match("%s%s") then + result.reason = "Your new name can't have consecutive spaces." return result end -- just copied from znote aac. local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do + for _, word in ipairs(words) do + for _, nameWord in ipairs(split) do if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. '" in your new name.' + result.reason = "You can't use the word '" .. word .. "' in your new name." return result end end end local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if tmpName:lower():find(words[i]) then - result.reason = "You can't use word \"" .. words[i] .. '" with whitespace in your new name.' + for _, word in ipairs(words) do + if tmpName:lower():find(word) then + result.reason = "You can't use the word '" .. word .. "' even with spaces in your new name." return result end end if MonsterType(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a monster's name." + result.reason = "Your new name '" .. name .. "' can't be a monster's name." return result elseif Npc(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a npc's name." + result.reason = "Your new name '" .. name .. "' can't be an NPC's name." return result end local letters = "{}|_*+-=<>0123456789@#%^&()/*'\\.,:;~!\"$" for i = 1, letters:len() do local c = letters:sub(i, i) - for i = 1, name:len() do - local m = name:sub(i, i) + for j = 1, name:len() do + local m = name:sub(j, j) if m == c then - result.reason = "You can't use this letter \"" .. c .. '" in your new name.' + result.reason = "You can't use this character '" .. c .. "' in your new name." return result end end end + result.ability = true return result end diff --git a/src/game/game.cpp b/src/game/game.cpp index 8b19f8bb94f..a4a53738b9b 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1092,7 +1092,7 @@ std::string Game::getPlayerNameByGUID(const uint32_t &guid) { ReturnValue Game::getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player) { size_t strlen = s.length(); - if (strlen == 0 || strlen > 20) { + if (strlen == 0 || strlen > 29) { return RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE; } From 444ad6cf640c3308c16f212d53e60e8b142cfd9c Mon Sep 17 00:00:00 2001 From: Felipe Pessoa Date: Tue, 31 Dec 2024 13:34:17 -0300 Subject: [PATCH 23/24] fix: lava lurker heals on fire damage (#3176) --- data-otservbr-global/monster/elementals/lava_lurker.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data-otservbr-global/monster/elementals/lava_lurker.lua b/data-otservbr-global/monster/elementals/lava_lurker.lua index 6a6e236f8c8..f7a88445c56 100644 --- a/data-otservbr-global/monster/elementals/lava_lurker.lua +++ b/data-otservbr-global/monster/elementals/lava_lurker.lua @@ -95,7 +95,7 @@ monster.elements = { { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, { type = COMBAT_ENERGYDAMAGE, percent = 0 }, { type = COMBAT_EARTHDAMAGE, percent = 0 }, - { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, { type = COMBAT_LIFEDRAIN, percent = 0 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, @@ -111,4 +111,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +monster.heals = { + { type = COMBAT_FIREDAMAGE, percent = 100 }, +} + mType:register(monster) From c61b1b4b7a805dac60ac19037c0d785dec0cede4 Mon Sep 17 00:00:00 2001 From: Filipe Arruda <66535966+arrudaqs@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:35:35 -0300 Subject: [PATCH 24/24] fix: add missing storage breaking the ice quest (#3201) --- data-otservbr-global/lib/core/storages.lua | 1 + .../scripts/creaturescripts/customs/freequests.lua | 1 + data-otservbr-global/scripts/lib/register_actions.lua | 1 + 3 files changed, 3 insertions(+) diff --git a/data-otservbr-global/lib/core/storages.lua b/data-otservbr-global/lib/core/storages.lua index 4529c64f583..da86c8b9bb9 100644 --- a/data-otservbr-global/lib/core/storages.lua +++ b/data-otservbr-global/lib/core/storages.lua @@ -923,6 +923,7 @@ Storage = { NoblemanSecondAddon = 41308, FormorgarMinesHoistSkeleton = 41309, FormorgarMinesHoistChest = 41310, + PickAmount = 41311, }, }, U8_1 = { -- update 8.1 - Reserved Storages 41351 - 41650 diff --git a/data-otservbr-global/scripts/creaturescripts/customs/freequests.lua b/data-otservbr-global/scripts/creaturescripts/customs/freequests.lua index bfeac7acad0..9c318264b11 100644 --- a/data-otservbr-global/scripts/creaturescripts/customs/freequests.lua +++ b/data-otservbr-global/scripts/creaturescripts/customs/freequests.lua @@ -173,6 +173,7 @@ local questTable = { { storageName = "TheIceIslands.Mission10", storage = Storage.Quest.U8_0.TheIceIslands.Mission10, storageValue = 2 }, { storageName = "TheIceIslands.Mission11", storage = Storage.Quest.U8_0.TheIceIslands.Mission11, storageValue = 2 }, { storageName = "TheIceIslands.Mission12", storage = Storage.Quest.U8_0.TheIceIslands.Mission12, storageValue = 6 }, + { storageName = "TheIceIslands.PickAmount", storage = Storage.Quest.U8_0.TheIceIslands.PickAmount, storageValue = 3 }, { storageName = "TheIceIslands.yakchalDoor", storage = Storage.Quest.U8_0.TheIceIslands.yakchalDoor, storageValue = 1 }, { storageName = "TheInquisitionQuest.Questline", storage = Storage.Quest.U8_2.TheInquisitionQuest.Questline, storageValue = 25 }, { storageName = "TheInquisitionQuest.Mission01", storage = Storage.Quest.U8_2.TheInquisitionQuest.Mission01, storageValue = 7 }, diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index ec68c90e27c..d89772f7ce9 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -651,6 +651,7 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) --The Ice Islands Quest, Nibelor 1: Breaking the Ice local missionProgress = player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.Mission02) local pickAmount = player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.PickAmount) + if missionProgress < 1 or pickAmount >= 3 or player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.Questline) ~= 3 then return false end