From 393179ce34c8a44ecd94e9055bf19af7f61c9aa3 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 18:58:30 -0700 Subject: [PATCH] feat: implement OTBM Zones, encounters, and raids systems (#1712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces three new systems: • OTBM Zones: Enables direct editing of zones into the map binary, facilitating the creation of large or irregularly shaped zones. A companion PR has been submitted to RME (https://github.com/opentibiabr/remeres-map-editor/pull/56). • Encounters: Introduces a flexible framework for defining scripted fights, simplifying the API for easy understanding and implementation. Encounters are tied to Zones and can be triggered via a boss lever. See the Magma Bubble script in this PR for a comprehensive example. • Raids: A specialized subset of Encounters, Raids are self-scheduling and can spawn within defined zones. This PR replaces Thais' Wild Horse and Rat raids with examples utilizing the new system. --- data-otservbr-global/raids/raids.xml | 2 - .../scripts/globalevents/spawn/raids.lua | 6 - .../scripts/lib/register_actions.lua | 3 +- .../scripts/lib/register_monster_type.lua | 5 + .../magma_bubble_fight.lua | 103 ++++--- .../scripts/raids/thais/rats.lua | 41 +++ .../scripts/raids/thais/wild_horses.lua | 27 ++ data/events/events.xml | 1 + data/libs/encounters_lib.lua | 157 ++++++---- data/libs/functions/bosslever.lua | 9 +- data/libs/functions/functions.lua | 11 + data/libs/hazard_lib.lua | 53 +++- data/libs/libs.lua | 1 + data/libs/raids_lib.lua | 145 ++++++++++ data/libs/zones_lib.lua | 35 ++- .../scripts/eventcallbacks/player/on_look.lua | 2 +- data/scripts/globalevents/encounters.lua | 13 + data/scripts/globalevents/raids.lua | 17 ++ src/canary_server.cpp | 2 + src/creatures/creature.cpp | 8 +- src/creatures/creature.hpp | 2 +- src/creatures/monsters/monsters.cpp | 6 +- src/creatures/monsters/monsters.hpp | 5 +- .../monsters/spawns/spawn_monster.cpp | 20 +- .../monsters/spawns/spawn_monster.hpp | 6 + src/creatures/npcs/npc.cpp | 4 +- src/creatures/npcs/spawns/spawn_npc.cpp | 3 +- src/creatures/players/grouping/party.cpp | 14 +- src/creatures/players/player.cpp | 15 +- src/creatures/players/player.hpp | 2 +- src/game/game.cpp | 103 ++++--- src/game/game.hpp | 8 +- src/game/zones/zone.cpp | 272 +++++++++--------- src/game/zones/zone.hpp | 135 +++++++-- src/io/functions/iologindata_load_player.cpp | 2 - src/io/io_definitions.hpp | 4 +- src/io/iobestiary.cpp | 2 +- src/io/iomap.cpp | 79 +++-- src/io/iomap.hpp | 32 +++ src/io/iomapserialize.cpp | 9 +- src/items/item.cpp | 1 - src/items/tile.cpp | 47 ++- src/items/tile.hpp | 6 +- .../functions/core/game/game_functions.cpp | 40 ++- .../functions/core/game/zone_functions.cpp | 43 ++- .../functions/core/game/zone_functions.hpp | 6 +- .../monster/monster_type_functions.cpp | 19 ++ .../monster/monster_type_functions.hpp | 4 + src/lua/functions/lua_functions_loader.hpp | 7 + src/map/map.cpp | 44 ++- src/map/map.hpp | 11 +- src/map/mapcache.cpp | 4 + src/utils/utils_definitions.hpp | 3 + 53 files changed, 1159 insertions(+), 440 deletions(-) create mode 100644 data-otservbr-global/scripts/raids/thais/rats.lua create mode 100644 data-otservbr-global/scripts/raids/thais/wild_horses.lua create mode 100644 data/libs/raids_lib.lua create mode 100644 data/scripts/globalevents/encounters.lua create mode 100644 data/scripts/globalevents/raids.lua diff --git a/data-otservbr-global/raids/raids.xml b/data-otservbr-global/raids/raids.xml index 82af716354d..035aab91e6c 100644 --- a/data-otservbr-global/raids/raids.xml +++ b/data-otservbr-global/raids/raids.xml @@ -99,8 +99,6 @@ - - diff --git a/data-otservbr-global/scripts/globalevents/spawn/raids.lua b/data-otservbr-global/scripts/globalevents/spawn/raids.lua index 27b60628852..83d065349dd 100644 --- a/data-otservbr-global/scripts/globalevents/spawn/raids.lua +++ b/data-otservbr-global/scripts/globalevents/spawn/raids.lua @@ -1,10 +1,4 @@ local raids = { - -- Weekly - --Segunda-Feira - ["Monday"] = { - ["06:00"] = { raidName = "RatsThais" }, - }, - --Terça-Feira ["Tuesday"] = { ["16:00"] = { raidName = "Midnight Panther" }, diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index e931fa87027..1ba181553d7 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -465,8 +465,7 @@ function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) if table.contains(holes, target.itemid) then target:transform(target.itemid + 1) target:decay() - toPosition:moveDownstairs() - toPosition.y = toPosition.y - 1 + toPosition.z = toPosition.z + 1 if Tile(toPosition):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) return true diff --git a/data-otservbr-global/scripts/lib/register_monster_type.lua b/data-otservbr-global/scripts/lib/register_monster_type.lua index 21a10d94aac..609a4079e59 100644 --- a/data-otservbr-global/scripts/lib/register_monster_type.lua +++ b/data-otservbr-global/scripts/lib/register_monster_type.lua @@ -24,6 +24,11 @@ registerMonsterType.description = function(mtype, mask) mtype:nameDescription(mask.description) end end +registerMonsterType.variant = function(mtype, mask) + if mask.variant then + mtype:variant(mask.variant) + end +end registerMonsterType.experience = function(mtype, mask) if mask.experience then mtype:experience(mask.experience) diff --git a/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua b/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua index f13c55d89e4..91f57104d85 100644 --- a/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua +++ b/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua @@ -25,71 +25,67 @@ spawnZone:addArea({ x = 33647, y = 32900, z = 15 }, { x = 33659, y = 32913, z = local encounter = Encounter("Magma Bubble", { zone = bossZone, spawnZone = spawnZone, - timeToSpawnMonsters = 2, + timeToSpawnMonsters = "2s", }) -encounter:addStage({ - prepare = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've entered the volcano.") - end, - - start = function() - encounter:spawnMonsters({ - name = "The End of Days", - amount = 3, - event = "fight.magma-bubble.TheEndOfDaysHealth", - }) - encounter:spawnMonsters({ - name = "Magma Crystal", - event = "fight.magma-bubble.MagmaCrystalDeath", - positions = { - Position(33647, 32891, 15), - Position(33647, 32926, 15), - Position(33670, 32898, 15), - }, - }) - end, +function encounter:onReset(position) + encounter:removeMonsters() +end - finish = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The whole Volcano starts to vibrate! Prepare yourself!") - end, +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("You've entered the volcano."):autoAdvance("1s") + +encounter:addSpawnMonsters({ + { + name = "The End of Days", + amount = 3, + event = "fight.magma-bubble.TheEndOfDaysHealth", + }, + { + name = "Magma Crystal", + event = "fight.magma-bubble.MagmaCrystalDeath", + positions = { + Position(33647, 32891, 15), + Position(33647, 32926, 15), + Position(33670, 32898, 15), + }, + }, }) -encounter:addIntermission(3000) - -encounter:addStage({ - start = function() - encounter:spawnMonsters({ - name = "The End of Days", - amount = 8, - event = "fight.magma-bubble.TheEndOfDaysDeath", - }) - end, +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("The whole Volcano starts to vibrate! Prepare yourself!"):autoAdvance("3s") - finish = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've upset the volcano and now it's going to take its revenge!") - end, +encounter:addSpawnMonsters({ + { + name = "The End of Days", + amount = 8, + event = "fight.magma-bubble.TheEndOfDaysDeath", + }, }) -encounter:addIntermission(3000) +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("You've upset the volcano and now it's going to take its revenge!"):autoAdvance("3s") -encounter:addStage({ - start = function() - encounter:spawnMonsters({ +encounter + :addSpawnMonsters({ + { name = "Magma Bubble", event = "fight.magma-bubble.MagmaBubbleDeath", positions = { Position(33654, 32909, 15), }, - }) - for i = 0, 4 do - table.insert(encounter.events, addEvent(encounter.spawnMonsters, (45 * i + 10) * 1000, encounter, { name = "Unchained Fire", amount = 5 })) - end - end, -}) + }, + }) + :autoAdvance("10s") -function encounter.beforeEach() - encounter:removeMonsters() +for i = 0, 4 do + local stage = encounter:addSpawnMonsters({ + { name = "Unchained Fire", amount = 5 }, + }) + + if i < 4 then + stage:autoAdvance("45s") + end end encounter:register() @@ -139,7 +135,7 @@ function overheatedDamage.onThink(interval, lastExecution) player:getPosition():sendMagicEffect(effect) else local damage = player:getMaxHealth() * 0.6 * -1 - doTargetCombatHealth(0, player, COMBAT_NEUTRALDAMAGE, damage, damage, CONST_ME_NONE) + doTargetCombatHealth(0, player, COMBAT_AGONYDAMAGE, damage, damage, CONST_ME_NONE) end ::continue:: end @@ -224,7 +220,8 @@ function chargedFlameAction.onUse(player, item, fromPosition, target, toPosition } local position = randomPosition(positions) position:sendMagicEffect(CONST_ME_FIREAREA) - Game.createItem(magicFieldId, 1, position) + local field = Game.createItem(magicFieldId, 1, position) + field:decay() item:remove() end @@ -271,7 +268,7 @@ function magmaCrystalDeath.onDeath() if crystals == 0 then encounter:nextStage() else - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "A magma crystal has been destroyed! " .. crystals .. " remaining.") + encounter:broadcast(MESSAGE_EVENT_ADVANCE, "A magma crystal has been destroyed! " .. crystals .. " remaining.") end end diff --git a/data-otservbr-global/scripts/raids/thais/rats.lua b/data-otservbr-global/scripts/raids/thais/rats.lua new file mode 100644 index 00000000000..c63b327462f --- /dev/null +++ b/data-otservbr-global/scripts/raids/thais/rats.lua @@ -0,0 +1,41 @@ +local zone = Zone("thais.rats") +zone:addArea(Position(32331, 32182, 7), Position(32426, 32261, 7)) + +local raid = Raid("thais.rats", { + zone = zone, + allowedDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + minActivePlayers = 0, + targetChancePerDay = 30, + maxChancePerCheck = 50, + minGapBetween = "36h", +}) + +raid:addBroadcast("Rat Plague in Thais!"):autoAdvance("5s") + +raid + :addSpawnMonsters({ + { + name = "Rat", + amount = 10, + }, + { + name = "Cave Rat", + amount = 10, + }, + }) + :autoAdvance("10m") + +raid + :addSpawnMonsters({ + { + name = "Rat", + amount = 20, + }, + { + name = "Cave Rat", + amount = 20, + }, + }) + :autoAdvance("10m") + +raid:register() diff --git a/data-otservbr-global/scripts/raids/thais/wild_horses.lua b/data-otservbr-global/scripts/raids/thais/wild_horses.lua new file mode 100644 index 00000000000..45ac7a3d262 --- /dev/null +++ b/data-otservbr-global/scripts/raids/thais/wild_horses.lua @@ -0,0 +1,27 @@ +local zone = Zone("thais.wild-horses") +zone:addArea(Position(32456, 32193, 7), Position(32491, 32261, 7)) +zone:addArea(Position(32431, 32240, 7), Position(32464, 32280, 7)) + +local raid = Raid("thais.wild-horses", { + zone = zone, + allowedDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + minActivePlayers = 0, + initialChance = 30, + targetChancePerDay = 50, + maxChancePerCheck = 50, + maxChecksPerDay = 2, + minGapBetween = "23h", +}) + +for _ = 1, 7 do + raid + :addSpawnMonsters({ + { + name = "Wild Horse", + amount = 3, + }, + }) + :autoAdvance("3h") +end + +raid:register() diff --git a/data/events/events.xml b/data/events/events.xml index a0eceaa76c5..f719242c1aa 100644 --- a/data/events/events.xml +++ b/data/events/events.xml @@ -15,6 +15,7 @@ Therefore, we strongly encourage avoiding the use of this file when possible, as + diff --git a/data/libs/encounters_lib.lua b/data/libs/encounters_lib.lua index 9085ea88010..1707799b8ac 100644 --- a/data/libs/encounters_lib.lua +++ b/data/libs/encounters_lib.lua @@ -5,8 +5,6 @@ ---@field finish function EncounterStage = {} -local unstarted = 0 - setmetatable(EncounterStage, { ---@param self EncounterStage ---@param config table @@ -20,18 +18,35 @@ setmetatable(EncounterStage, { end, }) +---Automatically advances to the next stage after the given delay +---@param delay number|string The delay time to advance to the next stage +function EncounterStage:autoAdvance(delay) + local originalStart = self.start + function self.start() + delay = delay or 50 -- 50ms is minimum delay; used here for close to instant advance + originalStart() + self.encounter:debug("Encounter[{}]:autoAdvance | next stage in: {}", self.encounter.name, delay == 50 and "instant" or delay) + self.encounter:addEvent(function() + self.encounter:nextStage() + end, delay) + end +end + ---@class Encounter ---@field name string ----@field private zone Zone ----@field private spawnZone Zone ----@field private stages EncounterStage[] ----@field private currentStage number ----@field private events table ----@field private registered boolean ----@field private timeToSpawnMonsters number ----@field beforeEach function +---@field protected zone Zone +---@field protected spawnZone Zone +---@field protected stages EncounterStage[] +---@field protected currentStage number +---@field protected events table +---@field protected registered boolean +---@field protected global boolean +---@field protected timeToSpawnMonsters number|string +---@field onReset function Encounter = { registry = {}, + unstarted = 0, + enableDebug = true, } setmetatable(Encounter, { @@ -57,17 +72,17 @@ setmetatable(Encounter, { end, }) +---@alias EncounterConfig { zone: Zone, spawnZone: Zone, global: boolean, timeToSpawnMonsters: number } ---Resets the encounter configuration ----@param config table The new configuration +---@param config EncounterConfig The new configuration function Encounter:resetConfig(config) self.zone = config.zone self.spawnZone = config.spawnZone or config.zone self.stages = {} - self.currentStage = unstarted - self.beforeEach = config.beforeEach + self.currentStage = Encounter.unstarted self.registered = false self.global = config.global or false - self.timeToSpawnMonsters = config.timeToSpawnMonsters or 3 + self.timeToSpawnMonsters = ParseDuration(config.timeToSpawnMonsters or "3s") self.events = {} end @@ -78,7 +93,7 @@ function Encounter:addEvent(callable, delay, ...) local event = addEvent(function(callable, ...) pcall(callable, ...) table.remove(self.events, index) - end, delay, callable, ...) + end, ParseDuration(delay), callable, ...) table.insert(self.events, index, event) end @@ -102,6 +117,7 @@ end ---@param abort boolean? A flag to determine whether to abort the current stage without calling the finish function. Optional. ---@return boolean True if the stage is entered successfully, false otherwise function Encounter:enterStage(stageNumber, abort) + self:debug("Encounter[{}]:enterStage | stageNumber: {} | abort: {}", self.name, stageNumber, abort) if not abort then local currentStage = self:getStage(self.currentStage) if currentStage and currentStage.finish then @@ -110,12 +126,9 @@ function Encounter:enterStage(stageNumber, abort) end self:cancelEvents() - if self.beforeEach then - self:beforeEach() - end - if stageNumber == unstarted then - self.currentStage = unstarted + if stageNumber == Encounter.unstarted then + self.currentStage = Encounter.unstarted return true end @@ -133,8 +146,10 @@ function Encounter:enterStage(stageNumber, abort) return true end +---@alias SpawnMonsterConfig { name: string, amount: number, event: string?, timeLimit: number?, position: Position|table?, positions: Position|table[]?, spawn: function? } + ---Spawns monsters based on the given configuration ----@param config {name: string, amount: number, event: string?, timeLimit: number?, position: Position|table?, positions: Position|table[]?, spawn: function?} The configuration for spawning monsters +---@param config SpawnMonsterConfig The configuration for spawning monsters function Encounter:spawnMonsters(config) local positions = config.positions local amount = config.amount @@ -155,7 +170,7 @@ function Encounter:spawnMonsters(config) end end for _, position in ipairs(positions) do - for i = 1, self.timeToSpawnMonsters do + for i = 1, self.timeToSpawnMonsters / 1000 do self:addEvent(function(position) position:sendMagicEffect(CONST_ME_TELEPORT) end, i * 1000, position) @@ -180,15 +195,19 @@ function Encounter:spawnMonsters(config) monster:remove() end, config.timeLimit, monster:getID()) end - end, self.timeToSpawnMonsters * 1000, config.name, position, config.event, config.spawn, config.timeLimit) + end, self.timeToSpawnMonsters, config.name, position, config.event, config.spawn, config.timeLimit) end end ---Broadcasts a message to all players function Encounter:broadcast(...) - for _, player in ipairs(Game.getPlayers()) do - player:sendTextMessage(...) + if self.global then + for _, player in ipairs(Game.getPlayers()) do + player:sendTextMessage(...) + end + return end + self.zone:sendTextMessage(...) end ---Counts the number of monsters with the given name in the encounter zone @@ -204,11 +223,6 @@ function Encounter:countPlayers() return self.zone:countPlayers(IgnoredByMonsters) end ----Sends a text message to all creatures in the encounter zone -function Encounter:sendTextMessage(...) - self.zone:sendTextMessage(...) -end - ---Removes all monsters from the encounter zone function Encounter:removeMonsters() self.zone:removeMonsters() @@ -217,10 +231,14 @@ end ---Resets the encounter to its initial state ---@return boolean True if the encounter is reset successfully, false otherwise function Encounter:reset() - if self.currentStage == unstarted then + if self.currentStage == Encounter.unstarted then return true end - return self:enterStage(unstarted) + self:debug("Encounter[{}]:reset", self.name) + if self.onReset then + self:onReset() + end + return self:enterStage(Encounter.unstarted) end ---Checks if a position is inside the encounter zone @@ -248,23 +266,25 @@ end ---Starts the encounter ---@return boolean True if the encounter is started successfully, false otherwise function Encounter:start() - Encounter.registerTickEvent() - if self.currentStage ~= unstarted then + if self.currentStage ~= Encounter.unstarted then return false end + self:debug("Encounter[{}]:start", self.name) return self:enterStage(1) end ---Adds a new stage to the encounter ----@param stage table The stage to add +---@param config table The stage to add ---@return boolean True if the stage is added successfully, false otherwise -function Encounter:addStage(stage) - table.insert(self.stages, EncounterStage(stage)) - return true +function Encounter:addStage(config) + local stage = EncounterStage(config) + stage.encounter = self + table.insert(self.stages, stage) + return stage end ---Adds an intermission stage to the encounter ----@param interval number The duration of the intermission +---@param interval number|string The duration of the intermission ---@return boolean True if the intermission stage is added successfully, false otherwise function Encounter:addIntermission(interval) return self:addStage({ @@ -276,6 +296,47 @@ function Encounter:addIntermission(interval) }) end +---Adds a stage that just sends a message to all players +---@param message string The message to send +---@return boolean True if the message stage is added successfully, false otherwise +function Encounter:addBroadcast(message, type) + type = type or MESSAGE_EVENT_ADVANCE + return self:addStage({ + start = function() + self:broadcast(type, message) + end, + }) +end + +---Adds a stage that spawns monsters +---@param configs SpawnMonsterConfig[] The configurations for spawning monsters +---@return boolean True if the spawn monsters stage is added successfully, false otherwise +function Encounter:addSpawnMonsters(configs) + if not configs then + return false + end + if not configs[1] then + configs = { configs } + end -- convert single config to array + return self:addStage({ + start = function() + for _, config in ipairs(configs) do + self:spawnMonsters(config) + end + end, + }) +end + +---Adds a stage that removes all monsters from the encounter zone +---@return boolean True if the remove monsters stage is added successfully, false otherwise +function Encounter:addRemoveMonsters() + return self:addStage({ + start = function() + self:removeMonsters() + end, + }) +end + ---Automatically starts the encounter when players enter the zone function Encounter:startOnEnter() local zoneEvents = ZoneEvent(self.zone) @@ -321,21 +382,9 @@ function Encounter:register() return true end -function Encounter.registerTickEvent() - if Encounter.tick then +function Encounter:debug(...) + if not Encounter.enableDebug then return end - Encounter.tick = GlobalEvent("encounter.ticks.onThink") - function Encounter.tick.onThink(interval, lastExecution) - for _, encounter in pairs(Encounter.registry) do - local stage = encounter:getStage() - if stage and stage.tick then - stage.tick(encounter, interval, lastExecution) - end - end - return true - end - - Encounter.tick:interval(1000) - Encounter.tick:register() + logger.debug(...) end diff --git a/data/libs/functions/bosslever.lua b/data/libs/functions/bosslever.lua index ac6736fb394..0cb84ec18ae 100644 --- a/data/libs/functions/bosslever.lua +++ b/data/libs/functions/bosslever.lua @@ -16,6 +16,7 @@ ---@field private area {from: Position, to: Position} ---@field private monsters {name: string, pos: Position}[] ---@field private exit Position +---@field private encounter Encounter ---@field private timeoutEvent Event BossLever = {} @@ -151,10 +152,6 @@ function BossLever:onUse(player) return false end self.onUseExtra(creature) - if self.encounter then - local encounter = Encounter(self.encounter) - encounter:start() - end return true end) @@ -177,6 +174,10 @@ function BossLever:onUse(player) monster:registerEvent("BossLeverOnDeath") end lever:teleportPlayers() + if self.encounter then + local encounter = Encounter(self.encounter) + encounter:start() + end lever:setStorageAllPlayers(self.storage, os.time() + self.timeToFightAgain) if self.timeoutEvent then stopEvent(self.timeoutEvent) diff --git a/data/libs/functions/functions.lua b/data/libs/functions/functions.lua index 74dbba95985..42163bc88a3 100644 --- a/data/libs/functions/functions.lua +++ b/data/libs/functions/functions.lua @@ -1139,3 +1139,14 @@ end function toKey(str) return str:lower():gsub(" ", "-"):gsub("%s+", "") end + +function toboolean(value) + if type(value) == "boolean" then + return value + end + if value == "true" then + return true + elseif value == "false" then + return false + end +end diff --git a/data/libs/hazard_lib.lua b/data/libs/hazard_lib.lua index a93da2a82c0..dc8ec0a0d44 100644 --- a/data/libs/hazard_lib.lua +++ b/data/libs/hazard_lib.lua @@ -9,14 +9,16 @@ function Hazard.new(prototype) instance.from = prototype.from instance.to = prototype.to instance.maxLevel = prototype.maxLevel - instance.storageMax = prototype.storageMax - instance.storageCurrent = prototype.storageCurrent + instance.storageMax = prototype.storageMax ---@deprecated + instance.storageCurrent = prototype.storageCurrent ---@deprecated instance.crit = prototype.crit instance.dodge = prototype.dodge instance.damageBoost = prototype.damageBoost instance.zone = Zone(instance.name) - instance.zone:addArea(instance.from, instance.to) + if instance.from and instance.to then + instance.zone:addArea(instance.from, instance.to) + end setmetatable(instance, { __index = Hazard }) @@ -46,8 +48,12 @@ function Hazard:getHazardPlayerAndPoints(damageMap) end function Hazard:getPlayerCurrentLevel(player) - local fromStorage = player:getStorageValue(self.storageCurrent) - return fromStorage <= 0 and 1 or fromStorage + if self.storageCurrent then + local fromStorage = player:getStorageValue(self.storageCurrent) + return fromStorage <= 0 and 1 or fromStorage + end + local fromKV = player:kv():scoped(self.name):get("currentLevel") or 1 + return fromKV <= 0 and 1 or fromKV end function Hazard:setPlayerCurrentLevel(player, level) @@ -55,7 +61,11 @@ function Hazard:setPlayerCurrentLevel(player, level) if level > max then return false end - player:setStorageValue(self.storageCurrent, level) + if self.storageCurrent then + player:setStorageValue(self.storageCurrent, level) + else + player:kv():scoped(self.name):set("currentLevel", level) + end local zones = player:getZones() if not zones then return true @@ -74,15 +84,28 @@ function Hazard:setPlayerCurrentLevel(player, level) end function Hazard:getPlayerMaxLevel(player) - local fromStorage = player:getStorageValue(self.storageMax) - return fromStorage <= 0 and 1 or fromStorage + if self.storageMax then + local fromStorage = player:getStorageValue(self.storageMax) + return fromStorage <= 0 and 1 or fromStorage + end + local fromKV = player:kv():scoped(self.name):get("maxLevel") + return fromKV <= 0 and 1 or fromKV end function Hazard:levelUp(player) - local current = self:getPlayerCurrentLevel(player) - local max = self:getPlayerMaxLevel(player) + if self.storageMax and self.storageCurrent then + local current = self:getPlayerCurrentLevel(player) + local max = self:getPlayerMaxLevel(player) + if current == max then + self:setPlayerMaxLevel(player, max + 1) + end + return + end + + local current = player:kv(self.name):get("currentLevel") + local max = player:kv(self.name):get("maxLevel") if current == max then - self:setPlayerMaxLevel(player, max + 1) + player:kv(self.name):set("maxLevel", max + 1) end end @@ -90,7 +113,12 @@ function Hazard:setPlayerMaxLevel(player, level) if level > self.maxLevel then level = self.maxLevel end - player:setStorageValue(self.storageMax, level) + + if self.storageMax then + player:setStorageValue(self.storageMax, level) + return + end + player:kv():scoped(self.name):set("maxLevel", level) end function Hazard:isInZone(position) @@ -119,6 +147,7 @@ function Hazard:register() if not player then return end + logger.debug("Player {} entered hazard zone {}", player:getName(), zone:getName()) player:setHazardSystemPoints(self:getPlayerCurrentLevel(player)) end diff --git a/data/libs/libs.lua b/data/libs/libs.lua index b4efaf02b6a..ccc71dc8961 100644 --- a/data/libs/libs.lua +++ b/data/libs/libs.lua @@ -30,4 +30,5 @@ dofile(CORE_DIRECTORY .. "/libs/zones_lib.lua") dofile(CORE_DIRECTORY .. "/libs/hazard_lib.lua") dofile(CORE_DIRECTORY .. "/libs/loyalty_lib.lua") dofile(CORE_DIRECTORY .. "/libs/encounters_lib.lua") +dofile(CORE_DIRECTORY .. "/libs/raids_lib.lua") dofile(CORE_DIRECTORY .. "/libs/concoctions_lib.lua") diff --git a/data/libs/raids_lib.lua b/data/libs/raids_lib.lua new file mode 100644 index 00000000000..4f1ea9b8cdf --- /dev/null +++ b/data/libs/raids_lib.lua @@ -0,0 +1,145 @@ +---@alias Weekday 'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday'|'Saturday'|'Sunday + +---@class Raid : Encounter +---@field allowedDays Weekday|Weekday[] The days of the week the raid is allowed to start +---@field minActivePlayers number The minimum number of players required to start the raid +---@field initialChance number|nil The initial chance to start the raid +---@field targetChancePerDay number The chance per enabled day to start the raid +---@field maxChancePerCheck number The maximum chance to start the raid in a single check (1m) +---@field minGapBetween string|number The minimum gap between raids of this type in seconds +---@field maxChecksPerDay number The maximum number of checks per day +---@field kv KV +Raid = { + registry = {}, + checkInterval = "1m", + idleTime = "5m", +} + +-- Set the metatable so that Raid inherits from Encounter +setmetatable(Raid, { + __index = Encounter, + ---@param config { name: string, global: boolean, allowedDays: Weekday|Weekday[], minActivePlayers: number, targetChancePerDay: number, maxChancePerCheck: number, minGapBetween: string|number, initialChance: number, maxChecksPerDay: number } + __call = function(self, name, config) + config.global = true + local raid = setmetatable(Encounter(name, config), { __index = Raid }) + raid.allowedDays = config.allowedDays + raid.minActivePlayers = config.minActivePlayers + raid.targetChancePerDay = config.targetChancePerDay + raid.maxChancePerCheck = config.maxChancePerCheck + raid.minGapBetween = ParseDuration(config.minGapBetween) + raid.initialChance = config.initialChance + raid.maxChecksPerDay = config.maxChecksPerDay + raid.kv = kv.scoped("raids"):scoped(name) + return raid + end, +}) + +---Registers the raid +---@param self Raid The raid to register +---@return boolean True if the raid is registered successfully, false otherwise +function Raid:register() + Encounter.register(self) + Raid.registry[self.name] = self + self.registered = true + return true +end + +---Starts the raid if it can be started +---@param self Raid The raid to try to start +---@return boolean True if the raid was started, false otherwise +function Raid:tryStart() + if not self:canStart() then + return false + end + logger.info("Starting raid {}", self.name) + self.kv:set("last-occurrence", os.time()) + self:start() + return true +end + +---Checks if the raid can be started +---@param self Raid The raid to check +---@return boolean True if the raid can be started, false otherwise +function Raid:canStart() + if self.currentStage ~= Encounter.unstarted then + logger.debug("Raid {} is already running", self.name) + return false + end + if self.allowedDays and not self:isAllowedDay() then + logger.debug("Raid {} is not allowed today ({})", self.name, os.date("%A")) + return false + end + if self.minActivePlayers and self:getActivePlayerCount() < self.minActivePlayers then + logger.debug("Raid {} does not have enough players (active: {}, min: {})", self.name, self:getActivePlayerCount(), self.minActivePlayers) + return false + end + local lastOccurrence = (self.kv:get("last-occurrence") or 0) * 1000 + local currentTime = os.time() * 1000 + if self.minGapBetween and lastOccurrence and currentTime - lastOccurrence < self.minGapBetween then + logger.debug("Raid {} occurred too recently (last: {} ago, min: {})", self.name, FormatDuration(currentTime - lastOccurrence), FormatDuration(self.minGapBetween)) + return false + end + + if not self.targetChancePerDay or not self.maxChancePerCheck then + logger.debug("Raid {} does not have a chance configured (targetChancePerDay: {}, maxChancePerCheck: {})", self.name, self.targetChancePerDay, self.maxChancePerCheck) + return false + end + + local checksToday = tonumber(self.kv:get("checks-today") or 0) + if self.maxChecksPerDay and checksToday >= self.maxChecksPerDay then + logger.debug("Raid {} has already checked today (checks today: {}, max: {})", self.name, checksToday, self.maxChecksPerDay) + return false + end + self.kv:set("checks-today", checksToday + 1) + + local failedAttempts = self.kv:get("failed-attempts") or 0 + local checksPerDay = ParseDuration("23h") / ParseDuration(Raid.checkInterval) + local initialChance = self.initialChance or (self.targetChancePerDay / checksPerDay) + local chanceIncrease = (self.targetChancePerDay - initialChance) / checksPerDay + local chance = initialChance + (chanceIncrease * failedAttempts) + if chance > self.maxChancePerCheck then + chance = self.maxChancePerCheck + end + chance = chance * 1000 + + -- offset the chance by 1000 to allow for fractional chances + local roll = math.random(100 * 1000) + if roll > chance then + logger.debug("Raid {} failed to start (roll: {}, chance: {}, failed attempts: {})", self.name, roll, chance, failedAttempts) + self.kv:set("failed-attempts", failedAttempts + 1) + return false + end + self.kv:set("failed-attempts", 0) + return true +end + +---Checks if the raid is allowed to start today +---@param self Raid The raid to check +---@return boolean True if the raid is allowed to start today, false otherwise +function Raid:isAllowedDay() + local day = os.date("%A") + if self.allowedDays == day then + return true + end + if type(self.allowedDays) == "table" then + for _, allowedDay in pairs(self.allowedDays) do + if allowedDay == day then + return true + end + end + end + return false +end + +---Gets the number of players in the game +---@param self Raid The raid to check +---@return number The number of players in the game +function Raid:getActivePlayerCount() + local count = 0 + for _, player in pairs(Game.getPlayers()) do + if player:getIdleTime() < ParseDuration(Raid.idleTime) then + count = count + 1 + end + end + return count +end diff --git a/data/libs/zones_lib.lua b/data/libs/zones_lib.lua index 33565bc9780..833c4d2cd4b 100644 --- a/data/libs/zones_lib.lua +++ b/data/libs/zones_lib.lua @@ -11,6 +11,10 @@ function Zone:randomPosition() local positions = self:getPositions() + if #positions == 0 then + 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 @@ -71,6 +75,7 @@ end ---@field public beforeLeave function ---@field public afterEnter function ---@field public afterLeave function +---@field public onSpawn function ZoneEvent = {} setmetatable(ZoneEvent, { @@ -79,8 +84,6 @@ setmetatable(ZoneEvent, { local obj = {} setmetatable(obj, { __index = ZoneEvent }) obj.zone = zone - obj.onEnter = nil - obj.onLeave = nil return obj end, }) @@ -133,6 +136,22 @@ function ZoneEvent:register() afterLeave:register() end + + if self.onSpawn then + local afterEnter = EventCallback() + function afterEnter.zoneAfterCreatureEnter(zone, creature) + if zone ~= self.zone then + return true + end + local monster = creature:getMonster() + if not monster then + return true + end + self.onSpawn(monster, monster:getPosition()) + end + + afterEnter:register() + end end function Zone:blockFamiliars() @@ -154,3 +173,15 @@ function Zone:trapMonsters() event:register() end + +function Zone:monsterIcon(category, icon, count) + local event = ZoneEvent(self) + function event.afterEnter(_zone, creature) + if not creature:isMonster() then + return + end + creature:setIcon(category, icon, count) + end + + event:register() +end diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 359cc8a8bee..9424ebf5785 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -62,7 +62,7 @@ function callback.playerOnLook(player, thing, position, distance) description = string.format(str, description, thing:getHealth(), thing:getMaxHealth()) .. "." end - description = string.format("%s\nPosition: %d, %d, %d", description, position.x, position.y, position.z) + description = string.format("%s\nPosition: (%d, %d, %d)", description, position.x, position.y, position.z) if thing:isCreature() then local speedBase = thing:getBaseSpeed() diff --git a/data/scripts/globalevents/encounters.lua b/data/scripts/globalevents/encounters.lua new file mode 100644 index 00000000000..7fb46c73b23 --- /dev/null +++ b/data/scripts/globalevents/encounters.lua @@ -0,0 +1,13 @@ +local encounterTick = GlobalEvent("encounters.tick.onThink") +function encounterTick.onThink(interval, lastExecution) + for _, encounter in pairs(Encounter.registry) do + local stage = encounter:getStage() + if stage and stage.tick then + stage.tick(encounter, interval, lastExecution) + end + end + return true +end + +encounterTick:interval(1000) +encounterTick:register() diff --git a/data/scripts/globalevents/raids.lua b/data/scripts/globalevents/raids.lua new file mode 100644 index 00000000000..709baf53bce --- /dev/null +++ b/data/scripts/globalevents/raids.lua @@ -0,0 +1,17 @@ +local serverSaveTime = GetNextOccurrence(configManager.getString(configKeys.GLOBAL_SERVER_SAVE_TIME)) +local stopExecutionAt = serverSaveTime - ParseDuration("1h") / ParseDuration("1s") -- stop rolling raids 1 hour before server save +local raidCheck = GlobalEvent("raids.check.onThink") + +function raidCheck.onThink(interval, lastExecution) + if os.time() > stopExecutionAt then + return true + end + + for _, raid in pairs(Raid.registry) do + raid:tryStart() + end + return true +end + +raidCheck:interval(ParseDuration(Raid.checkInterval)) +raidCheck:register() diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 8476509c820..7d895cdaaa9 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -16,6 +16,7 @@ #include "creatures/players/storages/storages.hpp" #include "database/databasemanager.hpp" #include "game/game.hpp" +#include "game/zones/zone.hpp" #include "game/scheduling/dispatcher.hpp" #include "game/scheduling/events_scheduler.hpp" #include "io/iomarket.hpp" @@ -153,6 +154,7 @@ void CanaryServer::loadMaps() const { if (g_configManager().getBoolean(TOGGLE_MAP_CUSTOM)) { g_game().loadCustomMaps(g_configManager().getString(DATA_DIRECTORY) + "/world/custom/"); } + Zone::refreshAll(); } catch (const std::exception &err) { throw FailedToInitializeCanary(err.what()); } diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 9825f50076a..e0df6f8b80f 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -1789,8 +1789,12 @@ void Creature::setIncreasePercent(CombatType_t combat, int32_t value) { } } -const phmap::parallel_flat_hash_set> Creature::getZones() { - return Zone::getZones(getPosition()); +phmap::flat_hash_set> Creature::getZones() { + auto tile = getTile(); + if (tile) { + return tile->getZones(); + } + return {}; } void Creature::iconChanged() { diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 9dd5745c353..0195ec17c30 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -263,7 +263,7 @@ class Creature : virtual public Thing, public SharedObject { return ZONE_NORMAL; } - const phmap::parallel_flat_hash_set> getZones(); + phmap::flat_hash_set> getZones(); // walk functions void startAutoWalk(const std::forward_list &listDir, bool ignoreConditions = false); diff --git a/src/creatures/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp index eb74178c0d0..ba05f5293d5 100644 --- a/src/creatures/monsters/monsters.cpp +++ b/src/creatures/monsters/monsters.cpp @@ -291,7 +291,7 @@ bool MonsterType::loadCallback(LuaScriptInterface* scriptInterface) { return true; } -std::shared_ptr Monsters::getMonsterType(const std::string &name) { +std::shared_ptr Monsters::getMonsterType(const std::string &name, bool silent /* = false*/) const { std::string lowerCaseName = asLowerCaseString(name); if (auto it = monsters.find(lowerCaseName); it != monsters.end() @@ -299,7 +299,9 @@ std::shared_ptr Monsters::getMonsterType(const std::string &name) { && it->first.find(lowerCaseName) != it->first.npos) { return it->second; } - g_logger().error("[Monsters::getMonsterType] - Monster with name {} not exist", lowerCaseName); + if (!silent) { + g_logger().error("[Monsters::getMonsterType] - Monster with name {} not exist", lowerCaseName); + } return nullptr; } diff --git a/src/creatures/monsters/monsters.hpp b/src/creatures/monsters/monsters.hpp index b214b478e3b..9f8a123e0e1 100644 --- a/src/creatures/monsters/monsters.hpp +++ b/src/creatures/monsters/monsters.hpp @@ -162,7 +162,7 @@ class MonsterType { public: MonsterType() = default; explicit MonsterType(const std::string &initName) : - name(initName), typeName(initName), nameDescription(initName) {}; + name(initName), typeName(initName), nameDescription(initName), variantName("") {}; // non-copyable MonsterType(const MonsterType &) = delete; @@ -173,6 +173,7 @@ class MonsterType { std::string name; std::string typeName; std::string nameDescription; + std::string variantName; MonsterInfo info; @@ -264,7 +265,7 @@ class Monsters { monsters.clear(); } - std::shared_ptr getMonsterType(const std::string &name); + std::shared_ptr getMonsterType(const std::string &name, bool silent = false) const; std::shared_ptr getMonsterTypeByRaceId(uint16_t raceId, bool isBoss = false) const; bool tryAddMonsterType(const std::string &name, const std::shared_ptr mType); bool deserializeSpell(const std::shared_ptr spell, spellBlock_t &sb, const std::string &description = ""); diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index 85de96aed32..aa9bcb14cf0 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -18,6 +18,7 @@ #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" #include "utils/pugicast.hpp" +#include "game/zones/zone.hpp" #include "map/spectators.hpp" static constexpr int32_t MONSTER_MINSPAWN_INTERVAL = 1000; // 1 second @@ -257,7 +258,7 @@ void SpawnMonster::cleanup() { while (it != spawnedMonsterMap.end()) { uint32_t spawnMonsterId = it->first; std::shared_ptr monster = it->second; - if (monster->isRemoved()) { + if (!monster || monster->isRemoved()) { spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME(); it = spawnedMonsterMap.erase(it); } else { @@ -267,7 +268,14 @@ void SpawnMonster::cleanup() { } bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval) { - const auto monsterType = g_monsters().getMonsterType(name); + std::string variant = ""; + for (const auto &zone : Zone::getZones(pos)) { + if (!zone->getMonsterVariant().empty()) { + variant = zone->getMonsterVariant() + "|"; + break; + } + } + const auto monsterType = g_monsters().getMonsterType(variant + name); if (!monsterType) { g_logger().error("Can not find {}", name); return false; @@ -296,6 +304,14 @@ void SpawnMonster::removeMonster(std::shared_ptr monster) { } } +void SpawnMonster::setMonsterVariant(const std::string &variant) { + for (auto &it : spawnMonsterMap) { + auto variantName = variant + it.second.monsterType->typeName; + auto variantType = g_monsters().getMonsterType(variantName, false); + it.second.monsterType = variantType ? variantType : it.second.monsterType; + } +} + void SpawnMonster::stopEvent() { if (checkSpawnMonsterEvent != 0) { g_dispatcher().stopEvent(checkSpawnMonsterEvent); diff --git a/src/creatures/monsters/spawns/spawn_monster.hpp b/src/creatures/monsters/spawns/spawn_monster.hpp index 3c361f95634..749856525d9 100644 --- a/src/creatures/monsters/spawns/spawn_monster.hpp +++ b/src/creatures/monsters/spawns/spawn_monster.hpp @@ -47,6 +47,12 @@ class SpawnMonster { bool isInSpawnMonsterZone(const Position &pos); void cleanup(); + const Position &getCenterPos() const { + return centerPos; + } + + void setMonsterVariant(const std::string &variant); + private: // map of the spawned creatures using SpawnedMap = std::multimap>; diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index 3e3e761536a..89eabdae28b 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -249,7 +249,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 if (std::shared_ptr tile = ignore ? player->getTile() : nullptr; tile) { double slotsNedeed = 0; if (itemType.stackable) { - slotsNedeed = inBackpacks ? std::ceil(std::ceil(static_cast(amount) / 100) / shoppingBagSlots) : std::ceil(static_cast(amount) / 100); + slotsNedeed = inBackpacks ? std::ceil(std::ceil(static_cast(amount) / itemType.stackSize) / shoppingBagSlots) : std::ceil(static_cast(amount) / itemType.stackSize); } else { slotsNedeed = inBackpacks ? std::ceil(static_cast(amount) / shoppingBagSlots) : static_cast(amount); } @@ -271,7 +271,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 uint32_t totalCost = buyPrice * amount; uint32_t bagsCost = 0; if (inBackpacks && itemType.stackable) { - bagsCost = shoppingBagPrice * static_cast(std::ceil(std::ceil(static_cast(amount) / 100) / shoppingBagSlots)); + bagsCost = shoppingBagPrice * static_cast(std::ceil(std::ceil(static_cast(amount) / itemType.stackSize) / shoppingBagSlots)); } else if (inBackpacks && !itemType.stackable) { bagsCost = shoppingBagPrice * static_cast(std::ceil(static_cast(amount) / shoppingBagSlots)); } diff --git a/src/creatures/npcs/spawns/spawn_npc.cpp b/src/creatures/npcs/spawns/spawn_npc.cpp index e30ed3b2abe..68fa0c2407b 100644 --- a/src/creatures/npcs/spawns/spawn_npc.cpp +++ b/src/creatures/npcs/spawns/spawn_npc.cpp @@ -137,7 +137,8 @@ void SpawnNpc::startSpawnNpcCheck() { SpawnNpc::~SpawnNpc() { for (const auto &it : spawnedNpcMap) { - it.second->setSpawnNpc(nullptr); + auto npc = it.second; + npc->setSpawnNpc(nullptr); } } diff --git a/src/creatures/players/grouping/party.cpp b/src/creatures/players/grouping/party.cpp index 57cfab4da75..b0a67a8eaf3 100644 --- a/src/creatures/players/grouping/party.cpp +++ b/src/creatures/players/grouping/party.cpp @@ -139,11 +139,6 @@ bool Party::leaveParty(std::shared_ptr player) { g_game().updatePlayerHelpers(member); } - leader->sendCreatureSkull(player); - player->sendCreatureSkull(player); - player->sendPlayerPartyIcons(leader); - leader->sendPartyCreatureUpdate(player); - player->sendTextMessage(MESSAGE_PARTY_MANAGEMENT, "You have left the party."); updateSharedExperience(); @@ -158,6 +153,11 @@ bool Party::leaveParty(std::shared_ptr player) { disband(); } + player->sendCreatureSkull(player); + leader->sendCreatureSkull(player); + player->sendPlayerPartyIcons(leader); + leader->sendPartyCreatureUpdate(player); + return true; } @@ -233,11 +233,13 @@ bool Party::joinParty(const std::shared_ptr &player) { for (auto member : getMembers()) { member->sendCreatureSkull(player); + member->sendPlayerPartyIcons(player); player->sendPlayerPartyIcons(member); } - player->sendCreatureSkull(player); leader->sendCreatureSkull(player); + player->sendCreatureSkull(player); + leader->sendPlayerPartyIcons(player); player->sendPlayerPartyIcons(leader); memberList.push_back(player); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 996c8de5d41..7dd02371688 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -1277,6 +1277,7 @@ void Player::sendPing() { } if (noPongTime >= 60000 && canLogout() && g_creatureEvents().playerLogout(static_self_cast())) { + g_logger().info("Player {} has been kicked due to ping timeout. (has client: {})", getName(), client != nullptr); if (client) { client->logout(true, true); } else { @@ -1596,6 +1597,11 @@ void Player::onCreatureAppear(std::shared_ptr creature, bool isLogin) } } + // Refresh bosstiary tracker onLogin + refreshCyclopediaMonsterTracker(true); + // Refresh bestiary tracker onLogin + refreshCyclopediaMonsterTracker(false); + for (const auto &condition : storedConditionList) { addCondition(condition); } @@ -1716,9 +1722,8 @@ void Player::onAttackedCreatureChangeZone(ZoneType_t zone) { void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) { Creature::onRemoveCreature(creature, isLogout); - auto player = getPlayer(); - if (creature == player) { + if (auto player = getPlayer(); player == creature) { if (isLogout) { if (party) { party->leaveParty(player); @@ -1731,7 +1736,7 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) loginPosition = getPosition(); lastLogout = time(nullptr); g_logger().info("{} has logged out", getName()); - g_chat().removeUserFromAllChannels(getPlayer()); + g_chat().removeUserFromAllChannels(player); clearPartyInvitations(); IOLoginData::updateOnlineStatus(guid, false); } @@ -4854,8 +4859,8 @@ bool Player::canFamiliar(uint16_t lookType) const { } for (const FamiliarEntry &familiarEntry : familiars) { - if (familiarEntry.lookType != lookType) { - continue; + if (familiarEntry.lookType == lookType) { + return true; } } return false; diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 306fed3f2b3..74398777104 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -490,7 +490,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void addStorageValueByName(const std::string &storageName, const int32_t value, const bool isLogin = false); std::shared_ptr kv() const { - return g_kv().scoped("player")->scoped(fmt::format("{}", getID())); + return g_kv().scoped("player")->scoped(fmt::format("{}", getGUID())); } void genReservedStorageRange(); diff --git a/src/game/game.cpp b/src/game/game.cpp index 776a1483bfd..bb5e28a1e5d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -419,7 +419,7 @@ bool Game::loadItemsPrice() { void Game::loadMainMap(const std::string &filename) { Monster::despawnRange = g_configManager().getNumber(DEFAULT_DESPAWNRANGE); Monster::despawnRadius = g_configManager().getNumber(DEFAULT_DESPAWNRADIUS); - map.loadMap(g_configManager().getString(DATA_DIRECTORY) + "/world/" + filename + ".otbm", true, true, true, true); + map.loadMap(g_configManager().getString(DATA_DIRECTORY) + "/world/" + filename + ".otbm", true, true, true, true, true); } void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { @@ -460,7 +460,7 @@ void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { continue; } - map.loadMapCustom(filename, true, true, true, customMapIndex); + map.loadMapCustom(filename, true, true, true, true, customMapIndex); customMapIndex++; } @@ -470,7 +470,7 @@ void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { } void Game::loadMap(const std::string &path, const Position &pos) { - map.loadMap(path, false, false, false, false, pos); + map.loadMap(path, false, false, false, false, false, pos); } std::shared_ptr Game::internalGetCylinder(std::shared_ptr player, const Position &pos) { @@ -700,7 +700,7 @@ std::shared_ptr Game::getPlayerByID(uint32_t id, bool loadTmp /* = false if (!loadTmp) { return nullptr; } - std::shared_ptr tmpPlayer(nullptr); + std::shared_ptr tmpPlayer = std::make_shared(nullptr); if (!IOLoginData::loadPlayerById(tmpPlayer, id)) { return nullptr; } @@ -768,7 +768,7 @@ std::shared_ptr Game::getPlayerByName(const std::string &s, bool loadTmp return it->second.lock(); } -std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid) { +std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid, bool loadTmp /* = false */) { if (guid == 0) { return nullptr; } @@ -777,7 +777,29 @@ std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid) { return it.second; } } - return nullptr; + if (!loadTmp) { + return nullptr; + } + std::shared_ptr tmpPlayer = std::make_shared(nullptr); + if (!IOLoginData::loadPlayerById(tmpPlayer, guid)) { + return nullptr; + } + return tmpPlayer; +} + +std::string Game::getPlayerNameByGUID(const uint32_t &guid) { + if (guid == 0) { + return ""; + } + if (m_playerNameCache.contains(guid)) { + return m_playerNameCache.at(guid); + } + auto player = getPlayerByGUID(guid, true); + auto name = player ? player->getName() : ""; + if (!name.empty()) { + m_playerNameCache[guid] = name; + } + return name; } ReturnValue Game::getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player) { @@ -819,7 +841,11 @@ bool Game::internalPlaceCreature(std::shared_ptr creature, const Posit if (creature->getParent() != nullptr) { return false; } - auto toZones = Zone::getZones(pos); + const auto &tile = map.getTile(pos); + if (!tile) { + return false; + } + auto toZones = tile->getZones(); if (auto ret = beforeCreatureZoneChange(creature, {}, toZones); ret != RETURNVALUE_NOERROR) { return false; } @@ -857,44 +883,52 @@ bool Game::placeCreature(std::shared_ptr creature, const Position &pos addCreatureCheck(creature); } - creature->getParent()->postAddNotification(creature, nullptr, 0); + auto parent = creature->getParent(); + if (parent) { + parent->postAddNotification(creature, nullptr, 0); + } creature->onPlacedCreature(); return true; } bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = true*/) { - if (creature->isRemoved()) { + if (!creature || creature->isRemoved()) { return false; } std::shared_ptr tile = creature->getTile(); + if (!tile) { + g_logger().error("[{}] tile on position '{}' for creature '{}' not exist", __FUNCTION__, creature->getPosition().toString(), creature->getName()); + } + auto fromZones = creature->getZones(); - std::vector oldStackPosVector; - - auto spectators = Spectators().find(tile->getPosition(), true); - auto playersSpectators = spectators.filter(); + if (tile) { + std::vector oldStackPosVector; + auto spectators = Spectators().find(tile->getPosition(), true); + auto playersSpectators = spectators.filter(); - for (const auto &spectator : playersSpectators) { - if (const auto &player = spectator->getPlayer()) { - oldStackPosVector.push_back(player->canSeeCreature(creature) ? tile->getStackposOfCreature(player, creature) : -1); + for (const auto &spectator : playersSpectators) { + if (const auto &player = spectator->getPlayer()) { + oldStackPosVector.push_back(player->canSeeCreature(creature) ? tile->getStackposOfCreature(player, creature) : -1); + } } - } - tile->removeCreature(creature); + tile->removeCreature(creature); - const Position &tilePosition = tile->getPosition(); + const Position &tilePosition = tile->getPosition(); - // Send to client - size_t i = 0; - for (const auto &spectator : playersSpectators) { - if (const auto &player = spectator->getPlayer()) { - player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); + // Send to client + size_t i = 0; + for (const auto &spectator : playersSpectators) { + if (const auto &player = spectator->getPlayer()) { + player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); + } } - } - // event method - for (auto spectator : spectators) { - spectator->onRemoveCreature(creature, isLogout); + // event method + for (auto spectator : spectators) { + spectator->onRemoveCreature(creature, isLogout); + } } if (creature->getMaster() && !creature->getMaster()->isRemoved()) { @@ -902,14 +936,14 @@ bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = } creature->getParent()->postRemoveNotification(creature, nullptr, 0); - afterCreatureZoneChange(creature, creature->getZones(), {}); + afterCreatureZoneChange(creature, fromZones, {}); creature->removeList(); creature->setRemoved(); removeCreatureCheck(creature); - for (const auto &summon : creature->m_summons) { + for (auto summon : creature->getSummons()) { summon->setSkillLoss(false); removeCreature(summon); } @@ -1475,7 +1509,8 @@ void Game::playerMoveItem(std::shared_ptr player, const Position &fromPo } } - if ((Position::getDistanceX(playerPos, mapToPos) > item->getThrowRange()) || (Position::getDistanceY(playerPos, mapToPos) > item->getThrowRange()) || (Position::getDistanceZ(mapFromPos, mapToPos) * 4 > item->getThrowRange())) { + auto throwRange = item->getThrowRange(); + if ((Position::getDistanceX(playerPos, mapToPos) > throwRange) || (Position::getDistanceY(playerPos, mapToPos) > throwRange) || (Position::getDistanceZ(mapFromPos, mapToPos) * 4 > throwRange)) { player->sendCancelMessage(RETURNVALUE_DESTINATIONOUTOFREACH); return; } @@ -9884,7 +9919,7 @@ void Game::setTransferPlayerHouseItems(uint32_t houseId, uint32_t playerId) { } template -phmap::parallel_flat_hash_set setDifference(const phmap::parallel_flat_hash_set &setA, const phmap::parallel_flat_hash_set &setB) { +phmap::parallel_flat_hash_set setDifference(const phmap::flat_hash_set &setA, const phmap::flat_hash_set &setB) { phmap::parallel_flat_hash_set setResult; for (const auto &elem : setA) { if (setB.find(elem) == setB.end()) { @@ -9894,7 +9929,7 @@ phmap::parallel_flat_hash_set setDifference(const phmap::parallel_flat_hash_s return setResult; } -ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones, bool force /* = false*/) const { +ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones, bool force /* = false*/) const { if (!creature) { return RETURNVALUE_NOTPOSSIBLE; } @@ -9924,7 +9959,7 @@ ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, c return RETURNVALUE_NOERROR; } -void Game::afterCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones) const { +void Game::afterCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones) const { if (!creature) { return; } diff --git a/src/game/game.hpp b/src/game/game.hpp index aa2d81ba831..e84cb4a513b 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -149,7 +149,9 @@ class Game { std::shared_ptr getPlayerByName(const std::string &s, bool allowOffline = false); - std::shared_ptr getPlayerByGUID(const uint32_t &guid); + std::shared_ptr getPlayerByGUID(const uint32_t &guid, bool allowOffline = false); + + std::string getPlayerNameByGUID(const uint32_t &guid); ReturnValue getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player); @@ -667,8 +669,8 @@ class Game { */ bool tryRetrieveStashItems(std::shared_ptr player, std::shared_ptr item); - ReturnValue beforeCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones, bool force = false) const; - void afterCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones) const; + ReturnValue beforeCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones, bool force = false) const; + void afterCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones) const; std::unique_ptr &getIOWheel(); const std::unique_ptr &getIOWheel() const; diff --git a/src/game/zones/zone.cpp b/src/game/zones/zone.cpp index f1aa300efee..fdeccb9bfaa 100644 --- a/src/game/zones/zone.cpp +++ b/src/game/zones/zone.cpp @@ -14,46 +14,51 @@ #include "creatures/monsters/monster.hpp" #include "creatures/npcs/npc.hpp" #include "creatures/players/player.hpp" +#include "utils/pugicast.hpp" phmap::parallel_flat_hash_map> Zone::zones = {}; +phmap::parallel_flat_hash_map> Zone::zonesByID = {}; const static std::shared_ptr nullZone = nullptr; -std::shared_ptr Zone::addZone(const std::string &name) { +std::shared_ptr Zone::addZone(const std::string &name, uint32_t zoneID /* = 0 */) { if (name == "default") { g_logger().error("Zone name {} is reserved", name); return nullZone; } + if (zoneID != 0 && zonesByID.contains(zoneID)) { + g_logger().debug("Found with ID {} while adding {}, linking them together...", zoneID, name); + auto zone = zonesByID[zoneID]; + zone->name = name; + zones[name] = zone; + return zone; + } + if (zones[name]) { g_logger().error("Zone {} already exists", name); return nullZone; } - zones[name] = std::make_shared(name); + zones[name] = std::make_shared(name, zoneID); + if (zoneID != 0) { + zonesByID[zoneID] = zones[name]; + } return zones[name]; } void Zone::addArea(Area area) { - for (const Position &pos : area) { - positions.insert(pos); + for (const auto &pos : area) { + addPosition(pos); } refresh(); } void Zone::subtractArea(Area area) { - for (const Position &pos : area) { - positions.erase(pos); - std::shared_ptr tile = g_game().map.getTile(pos); - if (tile) { - for (auto item : *tile->getItemList()) { - itemRemoved(item); - } - for (auto creature : *tile->getCreatures()) { - creatureRemoved(creature); - } - } + for (const auto &pos : area) { + removePosition(pos); } + refresh(); } -bool Zone::isPositionInZone(const Position &pos) const { +bool Zone::contains(const Position &pos) const { return positions.contains(pos); } @@ -74,115 +79,106 @@ std::shared_ptr Zone::getZone(const std::string &name) { return zones[name]; } -const phmap::parallel_flat_hash_set &Zone::getPositions() const { - return positions; +std::shared_ptr Zone::getZone(uint32_t zoneID) { + if (zoneID == 0) { + return nullZone; + } + if (zonesByID.contains(zoneID)) { + return zonesByID[zoneID]; + } + auto zone = std::make_shared(zoneID); + zonesByID[zoneID] = zone; + return zone; } -const phmap::parallel_flat_hash_set> &Zone::getTiles() const { - static phmap::parallel_flat_hash_set> tiles; - tiles.clear(); - for (const auto &position : positions) { - const auto tile = g_game().map.getTile(position); - if (tile) { - tiles.insert(tile); - } +std::vector Zone::getPositions() const { + std::vector result; + for (const auto &pos : positions) { + result.push_back(pos); } - return tiles; + return result; } -const phmap::parallel_flat_hash_set> &Zone::getCreatures() const { - static phmap::parallel_flat_hash_set> creatures; - creatures.clear(); - for (const auto creatureId : creaturesCache) { - const auto creature = g_game().getCreatureByID(creatureId); - if (creature) { - creatures.insert(creature); - } - } - return creatures; +std::vector> Zone::getCreatures() { + return weak::lock(creaturesCache); } -const phmap::parallel_flat_hash_set> &Zone::getPlayers() const { - static phmap::parallel_flat_hash_set> players; - players.clear(); - for (const auto playerId : playersCache) { - const auto player = g_game().getPlayerByID(playerId); - if (player) { - players.insert(player); - } - } - return players; +std::vector> Zone::getPlayers() { + return weak::lock(playersCache); } -const phmap::parallel_flat_hash_set> &Zone::getMonsters() const { - static phmap::parallel_flat_hash_set> monsters; - monsters.clear(); - for (const auto monsterId : monstersCache) { - const auto monster = g_game().getMonsterByID(monsterId); - if (monster) { - monsters.insert(monster); - } - } - return monsters; +std::vector> Zone::getMonsters() { + return weak::lock(monstersCache); } -const phmap::parallel_flat_hash_set> &Zone::getNpcs() const { - static phmap::parallel_flat_hash_set> npcs; - npcs.clear(); - for (const auto npcId : npcsCache) { - const auto npc = g_game().getNpcByID(npcId); - if (npc) { - npcs.insert(npc); - } - } - return npcs; +std::vector> Zone::getNpcs() { + return weak::lock(npcsCache); } -const phmap::parallel_flat_hash_set> &Zone::getItems() const { - return itemsCache; +std::vector> Zone::getItems() { + return weak::lock(itemsCache); } -void Zone::removePlayers() const { - for (auto player : getPlayers()) { +void Zone::removePlayers() { + for (const auto &player : getPlayers()) { g_game().internalTeleport(player, getRemoveDestination(player)); } } -void Zone::removeMonsters() const { - for (auto monster : getMonsters()) { - g_game().removeCreature(monster); +void Zone::removeMonsters() { + for (const auto &monster : getMonsters()) { + g_game().removeCreature(monster->getCreature()); } } -void Zone::removeNpcs() const { - for (auto npc : getNpcs()) { - g_game().removeCreature(npc); +void Zone::removeNpcs() { + for (const auto &npc : getNpcs()) { + g_game().removeCreature(npc->getCreature()); } } void Zone::clearZones() { + for (const auto &[_, zone] : zones) { + // do not clear zones loaded from the map (id > 0) + if (!zone || zone->isStatic()) { + continue; + } + zone->refresh(); + } zones.clear(); + for (const auto &[_, zone] : zonesByID) { + zones[zone->name] = zone; + } } -phmap::parallel_flat_hash_set> Zone::getZones(const Position postion) { - phmap::parallel_flat_hash_set> zonesSet; +std::vector> Zone::getZones(const Position position) { + Benchmark bm_getZones; + std::vector> result; for (const auto &[_, zone] : zones) { - if (zone && zone->isPositionInZone(postion)) { - zonesSet.insert(zone); + if (zone && zone->contains(position)) { + result.push_back(zone); } } - return zonesSet; + auto duration = bm_getZones.duration(); + if (duration > 100) { + g_logger().warn("Listed {} zones for position {} in {} milliseconds", result.size(), position.toString(), duration); + } + return result; } -const phmap::parallel_flat_hash_set> &Zone::getZones() { - static phmap::parallel_flat_hash_set> zonesSet; - zonesSet.clear(); +std::vector> Zone::getZones() { + Benchmark bm_getZones; + std::vector> result; for (const auto &[_, zone] : zones) { if (zone) { - zonesSet.insert(zone); + result.push_back(zone); } } - return zonesSet; + auto duration = bm_getZones.duration(); + if (duration > 100) { + g_logger().warn("Listed {} zones in {} milliseconds", result.size(), duration); + } + return result; } void Zone::creatureAdded(const std::shared_ptr &creature) { @@ -190,48 +186,25 @@ void Zone::creatureAdded(const std::shared_ptr &creature) { return; } - uint32_t id = 0; - if (creature->getPlayer()) { - id = creature->getPlayer()->getID(); - auto [_, playerInserted] = playersCache.insert(id); - if (playerInserted) { - g_logger().trace("Player {} (ID: {}) added to zone {}", creature->getName(), id, name); - } - } - if (creature->getMonster()) { - id = creature->getMonster()->getID(); - auto [_, monsterInserted] = monstersCache.insert(id); - if (monsterInserted) { - g_logger().trace("Monster {} (ID: {}) added to zone {}", creature->getName(), id, name); - } - } - if (creature->getNpc()) { - id = creature->getNpc()->getID(); - auto [_, npcInserted] = npcsCache.insert(id); - if (npcInserted) { - g_logger().trace("Npc {} (ID: {}) added to zone {}", creature->getName(), id, name); - } + if (const auto &player = creature->getPlayer()) { + playersCache.insert(player); + } else if (const auto &monster = creature->getMonster()) { + monstersCache.insert(monster); + } else if (const auto &npc = creature->getNpc()) { + npcsCache.insert(npc); } - if (id != 0) { - creaturesCache.insert(id); - } + creaturesCache.insert(creature); } void Zone::creatureRemoved(const std::shared_ptr &creature) { if (!creature) { return; } - creaturesCache.erase(creature->getID()); - if (creature->getPlayer() && playersCache.erase(creature->getID())) { - g_logger().trace("Player {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } - if (creature->getMonster() && monstersCache.erase(creature->getID())) { - g_logger().trace("Monster {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } - if (creature->getNpc() && npcsCache.erase(creature->getID())) { - g_logger().trace("Npc {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } + creaturesCache.erase(creature); + playersCache.erase(creature->getPlayer()); + monstersCache.erase(creature->getMonster()); + npcsCache.erase(creature->getNpc()); } void Zone::thingAdded(const std::shared_ptr &thing) { @@ -239,10 +212,10 @@ void Zone::thingAdded(const std::shared_ptr &thing) { return; } - if (thing->getItem()) { - itemAdded(thing->getItem()); - } else if (thing->getCreature()) { - creatureAdded(thing->getCreature()); + if (const auto &item = thing->getItem()) { + itemAdded(item); + } else if (const auto &creature = thing->getCreature()) { + creatureAdded(creature); } } @@ -261,30 +234,45 @@ void Zone::itemRemoved(const std::shared_ptr &item) { } void Zone::refresh() { + Benchmark bm_refresh; creaturesCache.clear(); monstersCache.clear(); npcsCache.clear(); playersCache.clear(); itemsCache.clear(); - for (const auto &position : positions) { - const auto tile = g_game().map.getTile(position); - if (!tile) { - continue; - } - const auto &items = tile->getItemList(); - if (!items) { - continue; - } - for (const auto &item : *items) { - itemAdded(item); - } - const auto &creatures = tile->getCreatures(); - if (!creatures) { + for (const auto &position : getPositions()) { + g_game().map.refreshZones(position); + } + g_logger().debug("Refreshed zone '{}' in {} milliseconds", name, bm_refresh.duration()); +} + +void Zone::setMonsterVariant(const std::string &variant) { + monsterVariant = variant; + g_logger().debug("Zone {} monster variant set to {}", name, variant); + for (auto &spawnMonster : g_game().map.spawnsMonster.getspawnMonsterList()) { + if (!contains(spawnMonster.getCenterPos())) { continue; } - for (const auto &creature : *creatures) { - creatureAdded(creature); - } + spawnMonster.setMonsterVariant(variant); + } + + removeMonsters(); +} + +bool Zone::loadFromXML(const std::string &fileName, uint16_t shiftID /* = 0 */) { + pugi::xml_document doc; + g_logger().debug("Loading zones from {}", fileName); + pugi::xml_parse_result result = doc.load_file(fileName.c_str()); + if (!result) { + printXMLError(__FUNCTION__, fileName, result); + return false; + } + + for (auto zoneNode : doc.child("zones").children()) { + auto name = zoneNode.attribute("name").value(); + auto zoneId = pugi::cast(zoneNode.attribute("zoneid").value()) << shiftID; + addZone(name, zoneId); } + return true; } diff --git a/src/game/zones/zone.hpp b/src/game/zones/zone.hpp index 0aa5e76fd0a..4a0c0318006 100644 --- a/src/game/zones/zone.hpp +++ b/src/game/zones/zone.hpp @@ -9,8 +9,10 @@ #pragma once +#include #include "game/movement/position.hpp" #include "items/item.hpp" +#include "creatures/creature.hpp" class Tile; class Creature; @@ -40,6 +42,10 @@ struct Area { Position from; Position to; + std::string toString() const { + return fmt::format("Area(from: {}, to: {})", from.toString(), to.toString()); + } + class PositionIterator { public: PositionIterator(Position startPosition, const Area &refArea) : @@ -79,10 +85,69 @@ struct Area { } }; +namespace weak { + template + struct ThingHasher { + std::size_t operator()(std::weak_ptr thing) const { + if (thing.expired()) { + return 0; + } + return std::hash {}(thing.lock().get()); + } + }; + + template + struct ThingComparator { + bool operator()(const std::weak_ptr &lhs, const std::weak_ptr &rhs) const { + return lhs.lock() == rhs.lock(); + } + }; + + template <> + struct ThingHasher { + std::size_t operator()(const std::weak_ptr &weakCreature) const { + auto locked = weakCreature.lock(); + if (!locked) { + return 0; + } + return std::hash {}(locked->getID()); + } + }; + + template <> + struct ThingComparator { + bool operator()(const std::weak_ptr &lhs, const std::weak_ptr &rhs) const { + if (lhs.expired() || rhs.expired()) { + return false; + } + return lhs.lock()->getID() == rhs.lock()->getID(); + } + }; + + template + using set = std::unordered_set, ThingHasher, ThingComparator>; + + template + std::vector> lock(set &weakSet) { + std::vector> result; + for (auto it = weakSet.begin(); it != weakSet.end();) { + if (it->expired()) { + it = weakSet.erase(it); + } else { + result.push_back(it->lock()); + ++it; + } + } + return result; + } +} + class Zone { public: - explicit Zone(const std::string &name) : - name(name) { } + explicit Zone(const std::string &name, uint32_t id = 0) : + name(name), id(id) { } + explicit Zone(uint32_t id) : + id(id) { } // Deleted copy constructor and assignment operator. Zone(const Zone &) = delete; @@ -93,19 +158,23 @@ class Zone { } void addArea(Area area); void subtractArea(Area area); - bool isPositionInZone(const Position &position) const; + void addPosition(const Position &position) { + positions.emplace(position); + } + void removePosition(const Position &position) { + positions.erase(position); + } Position getRemoveDestination(const std::shared_ptr &creature = nullptr) const; void setRemoveDestination(const Position &position) { removeDestination = position; } - const phmap::parallel_flat_hash_set &getPositions() const; - const phmap::parallel_flat_hash_set> &getTiles() const; - const phmap::parallel_flat_hash_set> &getCreatures() const; - const phmap::parallel_flat_hash_set> &getPlayers() const; - const phmap::parallel_flat_hash_set> &getMonsters() const; - const phmap::parallel_flat_hash_set> &getNpcs() const; - const phmap::parallel_flat_hash_set> &getItems() const; + std::vector getPositions() const; + std::vector> getCreatures(); + std::vector> getPlayers(); + std::vector> getMonsters(); + std::vector> getNpcs(); + std::vector> getItems(); void creatureAdded(const std::shared_ptr &creature); void creatureRemoved(const std::shared_ptr &creature); @@ -113,28 +182,50 @@ class Zone { void itemAdded(const std::shared_ptr &item); void itemRemoved(const std::shared_ptr &item); - void removePlayers() const; - void removeMonsters() const; - void removeNpcs() const; + void removePlayers(); + void removeMonsters(); + void removeNpcs(); void refresh(); - static std::shared_ptr addZone(const std::string &name); + void setMonsterVariant(const std::string &variant); + const std::string &getMonsterVariant() const { + return monsterVariant; + } + + bool isStatic() const { + return id != 0; + } + + static std::shared_ptr addZone(const std::string &name, uint32_t id = 0); static std::shared_ptr getZone(const std::string &name); - static phmap::parallel_flat_hash_set> getZones(const Position position); - const static phmap::parallel_flat_hash_set> &getZones(); + static std::shared_ptr getZone(uint32_t id); + static std::vector> getZones(const Position position); + static std::vector> getZones(); + static void refreshAll() { + for (const auto &[_, zone] : zones) { + zone->refresh(); + } + } static void clearZones(); + static bool loadFromXML(const std::string &fileName, uint16_t shiftID = 0); + private: + bool contains(const Position &position) const; + Position removeDestination = Position(); std::string name; - phmap::parallel_flat_hash_set positions; + std::string monsterVariant; + std::unordered_set positions; + uint32_t id = 0; // ID 0 is used in zones created dynamically from lua. The map editor uses IDs starting from 1 (automatically generated). - phmap::parallel_flat_hash_set> itemsCache; - phmap::parallel_flat_hash_set creaturesCache; - phmap::parallel_flat_hash_set monstersCache; - phmap::parallel_flat_hash_set npcsCache; - phmap::parallel_flat_hash_set playersCache; + weak::set itemsCache; + weak::set creaturesCache; + weak::set monstersCache; + weak::set npcsCache; + weak::set playersCache; static phmap::parallel_flat_hash_map> zones; + static phmap::parallel_flat_hash_map> zonesByID; }; diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 0b1cf0c1f3e..f404a52b86c 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -30,8 +30,6 @@ void IOLoginDataLoad::loadItems(ItemsMap &itemsMap, DBResult_ptr result, const s if (item) { if (!item->unserializeAttr(propStream)) { g_logger().warn("[{}] - Failed to deserialize item attributes {}, from player {}, from account id {}", __FUNCTION__, item->getID(), player->getName(), player->getAccountId()); - g_logger().info("[{}] - Deleting wrong item: {}", __FUNCTION__, item->getID()); - continue; } itemsMap[sid] = std::make_pair(item, pid); diff --git a/src/io/io_definitions.hpp b/src/io/io_definitions.hpp index 5a7d9bea6d4..22d61883ac8 100644 --- a/src/io/io_definitions.hpp +++ b/src/io/io_definitions.hpp @@ -45,7 +45,8 @@ enum OTBM_AttrTypes_t { OTBM_ATTR_SLEEPERGUID = 20, OTBM_ATTR_SLEEPSTART = 21, OTBM_ATTR_CHARGES = 22, - OTBM_ATTR_EXT_SPAWN_NPC_FILE = 23 + OTBM_ATTR_EXT_SPAWN_NPC_FILE = 23, + OTBM_ATTR_EXT_ZONE_FILE = 24, }; enum OTBM_NodeTypes_t { @@ -65,6 +66,7 @@ enum OTBM_NodeTypes_t { OTBM_HOUSETILE = 14, OTBM_WAYPOINTS = 15, OTBM_WAYPOINT = 16, + OTBM_TILE_ZONE = 19 }; enum OTBM_TileFlag_t : uint32_t { diff --git a/src/io/iobestiary.cpp b/src/io/iobestiary.cpp index 0797428898b..c9fb933bc8c 100644 --- a/src/io/iobestiary.cpp +++ b/src/io/iobestiary.cpp @@ -403,7 +403,7 @@ phmap::parallel_flat_hash_set IOBestiary::getBestiaryFinished(std::sha for (const auto &[monsterTypeRaceId, monsterTypeName] : bestiaryMap) { uint32_t thisKilled = player->getBestiaryKillCount(monsterTypeRaceId); auto mtype = g_monsters().getMonsterType(monsterTypeName); - if (mtype && thisKilled >= mtype->info.bestiaryFirstUnlock) { + if (mtype && thisKilled >= mtype->info.bestiaryToUnlock) { finishedMonsters.insert(monsterTypeRaceId); } } diff --git a/src/io/iomap.cpp b/src/io/iomap.cpp index 51306b7f756..f11e33204ce 100644 --- a/src/io/iomap.cpp +++ b/src/io/iomap.cpp @@ -103,6 +103,11 @@ void IOMap::parseMapDataAttributes(FileStream &stream, Map* map) { map->housefile += stream.getString(); } break; + case OTBM_ATTR_EXT_ZONE_FILE: { + map->zonesfile = map->path.string().substr(0, map->path.string().rfind('/') + 1); + map->zonesfile += stream.getString(); + } break; + default: stream.back(); end = true; @@ -182,36 +187,50 @@ void IOMap::parseTileArea(FileStream &stream, Map &map, const Position &pos) { } while (stream.startNode()) { - if (stream.getU8() != OTBM_ITEM) { - throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Could not read item node.", x, y, z)); - } - - const uint16_t id = stream.getU16(); - - const auto &iType = Item::items[id]; - - if (iType.blockSolid) { - tileIsStatic = true; - } - - const auto item = std::make_shared(); - item->id = id; - - if (!item->unserializeItemNode(stream, x, y, z)) { - throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Failed to load item {}, Node Type.", x, y, z, id)); - } - - if (tile->isHouse() && iType.isBed()) { - // nothing - } else if (tile->isHouse() && iType.moveable) { - g_logger().warn("[IOMap::loadMap] - " - "Moveable item with ID: {}, in house: {}, " - "at position: x {}, y {}, z {}", - id, tile->houseId, x, y, z); - } else if (iType.isGroundTile()) { - tile->ground = map.tryReplaceItemFromCache(item); - } else { - tile->items.emplace_back(map.tryReplaceItemFromCache(item)); + auto type = stream.getU8(); + switch (type) { + case OTBM_ITEM: { + const uint16_t id = stream.getU16(); + + const auto &iType = Item::items[id]; + + if (iType.blockSolid) { + tileIsStatic = true; + } + + const auto item = std::make_shared(); + item->id = id; + + if (!item->unserializeItemNode(stream, x, y, z)) { + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Failed to load item {}, Node Type.", x, y, z, id)); + } + + if (tile->isHouse() && iType.isBed()) { + // nothing + } else if (tile->isHouse() && iType.moveable) { + g_logger().warn("[IOMap::loadMap] - " + "Moveable item with ID: {}, in house: {}, " + "at position: x {}, y {}, z {}", + id, tile->houseId, x, y, z); + } else if (iType.isGroundTile()) { + tile->ground = map.tryReplaceItemFromCache(item); + } else { + tile->items.emplace_back(map.tryReplaceItemFromCache(item)); + } + } break; + case OTBM_TILE_ZONE: { + const auto zoneCount = stream.getU16(); + for (uint16_t i = 0; i < zoneCount; ++i) { + const auto zoneId = stream.getU16(); + if (!zoneId) { + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Invalid zone id.", x, y, z)); + } + auto zone = Zone::getZone(zoneId); + zone->addPosition(Position(x, y, z)); + } + } break; + default: + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Could not read item/zone node.", x, y, z)); } if (!stream.endNode()) { diff --git a/src/io/iomap.hpp b/src/io/iomap.hpp index d483ad58bb8..0c93b4a8e69 100644 --- a/src/io/iomap.hpp +++ b/src/io/iomap.hpp @@ -17,6 +17,7 @@ #include "map/map.hpp" #include "creatures/monsters/spawns/spawn_monster.hpp" #include "creatures/npcs/spawns/spawn_npc.hpp" +#include "game/zones/zone.hpp" class IOMap { public: @@ -38,6 +39,22 @@ class IOMap { return map->spawnsMonster.loadFromXML(map->monsterfile); } + /** + * Load main map zones + * \param map Is the map class + * \returns true if the zones spawn map was loaded successfully + */ + static bool loadZones(Map* map) { + if (map->zonesfile.empty()) { + // OTBM file doesn't tell us about the zonesfile, + // Lets guess it is mapname-zone.xml. + map->zonesfile = g_configManager().getString(MAP_NAME); + map->zonesfile += "-zones.xml"; + } + + return Zone::loadFromXML(map->zonesfile); + } + /** * Load main map npcs * \param map Is the map class @@ -85,6 +102,21 @@ class IOMap { return map->spawnsMonsterCustomMaps[customMapIndex].loadFromXML(map->monsterfile); } + /** + * Load custom map zones + * \param map Is the map class + * \returns true if the zones spawn map custom was loaded successfully + */ + static bool loadZonesCustom(Map* map, const std::string &mapName, int customMapIndex) { + if (map->zonesfile.empty()) { + // OTBM file doesn't tell us about the zonesfile, + // Lets guess it is mapname-zones.xml. + map->zonesfile = mapName; + map->zonesfile += "-zones.xml"; + } + return Zone::loadFromXML(map->zonesfile, customMapIndex); + } + /** * Load custom map npcs * \param map Is the map class diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index c089fe022db..8e35f5acb0d 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -77,7 +77,7 @@ bool IOMapSerialize::SaveHouseItemsGuard() { PropWriteStream stream; for (const auto &[key, house] : g_game().map.houses.getHouses()) { // save house items - for (std::shared_ptr tile : house->getTiles()) { + for (const auto &tile : house->getTiles()) { saveTile(stream, tile); size_t attributesSize; @@ -233,7 +233,12 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, std::shared_ptr til std::forward_list> items; uint16_t count = 0; for (auto &item : *tileItems) { - if (!item->isSavedToHouses()) { + if (item->getID() == ITEM_BATHTUB_FILLED_NOTMOVABLE) { + std::shared_ptr tub = Item::CreateItem(ITEM_BATHTUB_FILLED); + items.push_front(tub); + ++count; + continue; + } else if (!item->isSavedToHouses()) { continue; } diff --git a/src/items/item.cpp b/src/items/item.cpp index 1b60d82b4e8..1632cba95dc 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -3082,7 +3082,6 @@ void Item::startDecaying() { } void Item::stopDecaying() { - g_logger().debug("Item::stopDecaying"); g_decay().stopDecay(static_self_cast()); } diff --git a/src/items/tile.cpp b/src/items/tile.cpp index 0d9a5db0ead..92beb20d52f 100644 --- a/src/items/tile.cpp +++ b/src/items/tile.cpp @@ -461,7 +461,7 @@ void Tile::onRemoveTileItem(const CreatureVector &spectators, const std::vector< } } } - for (const auto zone : getZones()) { + for (auto &zone : getZones()) { zone->itemRemoved(item); } @@ -1732,6 +1732,47 @@ std::shared_ptr Tile::getDoorItem() const { return nullptr; } -const phmap::parallel_flat_hash_set> Tile::getZones() { - return Zone::getZones(getPosition()); +phmap::flat_hash_set> Tile::getZones() { + return zones; +} + +void Tile::addZone(std::shared_ptr zone) { + zones.insert(zone); + const auto &items = getItemList(); + if (items) { + for (const auto &item : *items) { + zone->itemAdded(item); + } + } + const auto &creatures = getCreatures(); + if (creatures) { + for (const auto &creature : *creatures) { + zone->creatureAdded(creature); + } + } +} + +void Tile::clearZones() { + phmap::flat_hash_set> zonesToRemove; + for (const auto &zone : zones) { + if (zone->isStatic()) { + continue; + } + zonesToRemove.insert(zone); + const auto &items = getItemList(); + if (items) { + for (const auto &item : *items) { + zone->itemRemoved(item); + } + } + const auto &creatures = getCreatures(); + if (creatures) { + for (const auto &creature : *creatures) { + zone->creatureRemoved(creature); + } + } + } + for (const auto &zone : zonesToRemove) { + zones.erase(zone); + } } diff --git a/src/items/tile.hpp b/src/items/tile.hpp index 2158d635de8..de5dd4f0e43 100644 --- a/src/items/tile.hpp +++ b/src/items/tile.hpp @@ -172,8 +172,10 @@ class Tile : public Cylinder, public SharedObject { void resetFlag(uint32_t flag) { this->flags &= ~flag; } + void addZone(std::shared_ptr zone); + void clearZones(); - const phmap::parallel_flat_hash_set> getZones(); + phmap::flat_hash_set> getZones(); ZoneType_t getZoneType() const { if (hasFlag(TILESTATE_PROTECTIONZONE)) { @@ -261,7 +263,7 @@ class Tile : public Cylinder, public SharedObject { std::shared_ptr ground = nullptr; Position tilePos; uint32_t flags = 0; - std::shared_ptr zone; + phmap::flat_hash_set> zones; }; // Used for walkable tiles, where there is high likeliness of diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 6e80bf88759..ea01813c66b 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -30,23 +30,47 @@ // Game int GameFunctions::luaGameCreateMonsterType(lua_State* L) { - // Game.createMonsterType(name) + // Game.createMonsterType(name[, variant = ""[, alternateName = ""]]) if (isString(L, 1)) { - std::string name = getString(L, 1); + const auto name = getString(L, 1); + std::string uniqueName = name; + auto variant = getString(L, 2, ""); + const auto alternateName = getString(L, 3, ""); + std::set names; auto monsterType = std::make_shared(name); - if (!g_monsters().tryAddMonsterType(name, monsterType)) { - lua_pushstring(L, fmt::format("The monster with name {} already registered", name).c_str()); - lua_error(L); - return 1; - } - if (!monsterType) { lua_pushstring(L, "MonsterType is nullptr"); lua_error(L); return 1; } + // if variant starts with !, then it's the only variant for this monster, so we register it with both names + if (variant.starts_with("!")) { + names.insert(name); + variant = variant.substr(1); + } + if (!variant.empty()) { + uniqueName = variant + "|" + name; + } + names.insert(uniqueName); + + monsterType->name = name; + if (!alternateName.empty()) { + names.insert(alternateName); + monsterType->name = alternateName; + } + + monsterType->variantName = variant; monsterType->nameDescription = "a " + name; + + for (const auto &alternateName : names) { + if (!g_monsters().tryAddMonsterType(alternateName, monsterType)) { + lua_pushstring(L, fmt::format("The monster with name {} already registered", alternateName).c_str()); + lua_error(L); + return 1; + } + } + pushUserdata(L, monsterType); setMetatable(L, -1, "MonsterType"); } else { diff --git a/src/lua/functions/core/game/zone_functions.cpp b/src/lua/functions/core/game/zone_functions.cpp index a624f36fe78..5471d01895a 100644 --- a/src/lua/functions/core/game/zone_functions.cpp +++ b/src/lua/functions/core/game/zone_functions.cpp @@ -121,27 +121,6 @@ int ZoneFunctions::luaZoneGetPositions(lua_State* L) { return 1; } -int ZoneFunctions::luaZoneGetTiles(lua_State* L) { - // Zone:getTiles() - auto zone = getUserdataShared(L, 1); - if (!zone) { - reportErrorFunc(getErrorDesc(LUA_ERROR_ZONE_NOT_FOUND)); - pushBoolean(L, false); - return 1; - } - auto tiles = zone->getTiles(); - lua_createtable(L, static_cast(tiles.size()), 0); - - int index = 0; - for (auto tile : tiles) { - index++; - pushUserdata(L, tile.get()); - setMetatable(L, -1, "Tile"); - lua_rawseti(L, -2, index); - } - return 1; -} - int ZoneFunctions::luaZoneGetCreatures(lua_State* L) { // Zone:getCreatures() auto zone = getUserdataShared(L, 1); @@ -284,6 +263,24 @@ int ZoneFunctions::luaZoneRemoveNpcs(lua_State* L) { return 1; } +int ZoneFunctions::luaZoneSetMonsterVariant(lua_State* L) { + // Zone:setMonsterVariant(variant) + auto zone = getUserdataShared(L, 1); + if (!zone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto variant = getString(L, 2); + if (variant.empty()) { + pushBoolean(L, false); + return 1; + } + zone->setMonsterVariant(variant); + pushBoolean(L, true); + return 1; +} + int ZoneFunctions::luaZoneGetByName(lua_State* L) { // Zone.getByName(name) auto name = getString(L, 1); @@ -306,7 +303,7 @@ int ZoneFunctions::luaZoneGetByPosition(lua_State* L) { return 1; } int index = 0; - const auto zones = tile->getZones(); + auto zones = tile->getZones(); lua_createtable(L, static_cast(zones.size()), 0); for (auto zone : zones) { index++; @@ -319,7 +316,7 @@ int ZoneFunctions::luaZoneGetByPosition(lua_State* L) { int ZoneFunctions::luaZoneGetAll(lua_State* L) { // Zone.getAll() - const auto zones = Zone::getZones(); + auto zones = Zone::getZones(); lua_createtable(L, static_cast(zones.size()), 0); int index = 0; for (auto zone : zones) { diff --git a/src/lua/functions/core/game/zone_functions.hpp b/src/lua/functions/core/game/zone_functions.hpp index c8ce215f6fd..2a3bbd0f8ea 100644 --- a/src/lua/functions/core/game/zone_functions.hpp +++ b/src/lua/functions/core/game/zone_functions.hpp @@ -16,7 +16,6 @@ class ZoneFunctions final : LuaScriptInterface { registerMethod(L, "Zone", "getRemoveDestination", ZoneFunctions::luaZoneGetRemoveDestination); registerMethod(L, "Zone", "setRemoveDestination", ZoneFunctions::luaZoneSetRemoveDestination); registerMethod(L, "Zone", "getPositions", ZoneFunctions::luaZoneGetPositions); - registerMethod(L, "Zone", "getTiles", ZoneFunctions::luaZoneGetTiles); registerMethod(L, "Zone", "getCreatures", ZoneFunctions::luaZoneGetCreatures); registerMethod(L, "Zone", "getPlayers", ZoneFunctions::luaZoneGetPlayers); registerMethod(L, "Zone", "getMonsters", ZoneFunctions::luaZoneGetMonsters); @@ -28,6 +27,8 @@ class ZoneFunctions final : LuaScriptInterface { registerMethod(L, "Zone", "removeNpcs", ZoneFunctions::luaZoneRemoveNpcs); registerMethod(L, "Zone", "refresh", ZoneFunctions::luaZoneRefresh); + registerMethod(L, "Zone", "setMonsterVariant", ZoneFunctions::luaZoneSetMonsterVariant); + // static methods registerMethod(L, "Zone", "getByPosition", ZoneFunctions::luaZoneGetByPosition); registerMethod(L, "Zone", "getByName", ZoneFunctions::luaZoneGetByName); @@ -45,7 +46,6 @@ class ZoneFunctions final : LuaScriptInterface { static int luaZoneSetRemoveDestination(lua_State* L); static int luaZoneRefresh(lua_State* L); static int luaZoneGetPositions(lua_State* L); - static int luaZoneGetTiles(lua_State* L); static int luaZoneGetCreatures(lua_State* L); static int luaZoneGetPlayers(lua_State* L); static int luaZoneGetMonsters(lua_State* L); @@ -56,6 +56,8 @@ class ZoneFunctions final : LuaScriptInterface { static int luaZoneRemoveMonsters(lua_State* L); static int luaZoneRemoveNpcs(lua_State* L); + static int luaZoneSetMonsterVariant(lua_State* L); + static int luaZoneGetByPosition(lua_State* L); static int luaZoneGetByName(lua_State* L); static int luaZoneGetAll(lua_State* L); diff --git a/src/lua/functions/creatures/monster/monster_type_functions.cpp b/src/lua/functions/creatures/monster/monster_type_functions.cpp index ba77c49a90a..d481b874c30 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.cpp @@ -1678,3 +1678,22 @@ int MonsterTypeFunctions::luaMonsterTypedeathSound(lua_State* L) { return 1; } + +int MonsterTypeFunctions::luaMonsterTypeVariant(lua_State* L) { + // get: monsterType:variant() set: monsterType:variant(variantName) + const auto monsterType = getUserdataShared(L, 1); + if (!monsterType) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + if (lua_gettop(L) == 1) { + pushString(L, monsterType->variantName); + } else { + monsterType->variantName = getString(L, 2); + pushBoolean(L, true); + } + + return 1; +} diff --git a/src/lua/functions/creatures/monster/monster_type_functions.hpp b/src/lua/functions/creatures/monster/monster_type_functions.hpp index 9d892219250..199b109daa5 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.hpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.hpp @@ -140,6 +140,8 @@ class MonsterTypeFunctions final : LuaScriptInterface { registerMethod(L, "MonsterType", "addSound", MonsterTypeFunctions::luaMonsterTypeAddSound); registerMethod(L, "MonsterType", "getSounds", MonsterTypeFunctions::luaMonsterTypeGetSounds); registerMethod(L, "MonsterType", "deathSound", MonsterTypeFunctions::luaMonsterTypedeathSound); + + registerMethod(L, "MonsterType", "variant", MonsterTypeFunctions::luaMonsterTypeVariant); } private: @@ -263,4 +265,6 @@ class MonsterTypeFunctions final : LuaScriptInterface { static int luaMonsterTypeGetSounds(lua_State* L); static int luaMonsterTypedeathSound(lua_State* L); static int luaMonsterTypeCritChance(lua_State* L); + + static int luaMonsterTypeVariant(lua_State* L); }; diff --git a/src/lua/functions/lua_functions_loader.hpp b/src/lua/functions/lua_functions_loader.hpp index 0fee2196553..89baf958e18 100644 --- a/src/lua/functions/lua_functions_loader.hpp +++ b/src/lua/functions/lua_functions_loader.hpp @@ -101,6 +101,13 @@ class LuaFunctionsLoader { static std::string getFormatedLoggerMessage(lua_State* L); static std::string getString(lua_State* L, int32_t arg); + static std::string getString(lua_State* L, int32_t arg, std::string defaultValue) { + const auto parameters = lua_gettop(L); + if (parameters == 0 || arg > parameters) { + return defaultValue; + } + return getString(L, arg); + } static CombatDamage getCombatDamage(lua_State* L); static Position getPosition(lua_State* L, int32_t arg, int32_t &stackpos); static Position getPosition(lua_State* L, int32_t arg); diff --git a/src/map/map.cpp b/src/map/map.cpp index 39fbe47872d..53f7c98db38 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -32,7 +32,7 @@ void Map::load(const std::string &identifier, const Position &pos) { } } -void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool loadHouses /*= false*/, bool loadMonsters /*= false*/, bool loadNpcs /*= false*/, const Position &pos /*= Position()*/) { +void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool loadHouses /*= false*/, bool loadMonsters /*= false*/, bool loadNpcs /*= false*/, bool loadZones /*= false*/, const Position &pos /*= Position()*/) { // Only download map if is loading the main map and it is not already downloaded if (mainMap && g_configManager().getBoolean(TOGGLE_DOWNLOAD_MAP) && !std::filesystem::exists(identifier)) { const auto mapDownloadUrl = g_configManager().getString(MAP_DOWNLOAD_URL); @@ -86,6 +86,10 @@ void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool IOMap::loadNpcs(this); } + if (loadZones) { + IOMap::loadZones(this); + } + // Files need to be cleaned up if custom map is enabled to open, or will try to load main map files if (g_configManager().getBoolean(TOGGLE_MAP_CUSTOM)) { monsterfile.clear(); @@ -94,7 +98,7 @@ void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool } } -void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, int customMapIndex) { +void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, bool loadZones, int customMapIndex) { // Load the map load(g_configManager().getString(DATA_DIRECTORY) + "/world/custom/" + mapName + ".otbm"); @@ -110,6 +114,10 @@ void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMo g_logger().warn("Failed to load npc custom spawn data"); } + if (loadZones && !IOMap::loadZonesCustom(this, mapName, customMapIndex)) { + g_logger().warn("Failed to load zones custom data"); + } + // Files need to be cleaned up or will try to load previous map files again monsterfile.clear(); housefile.clear(); @@ -149,6 +157,25 @@ std::shared_ptr Map::getOrCreateTile(uint16_t x, uint16_t y, uint8_t z, bo return tile; } +std::shared_ptr Map::getLoadedTile(uint16_t x, uint16_t y, uint8_t z) { + if (z >= MAP_MAX_LAYERS) { + return nullptr; + } + + const auto leaf = getQTNode(x, y); + if (!leaf) { + return nullptr; + } + + const auto &floor = leaf->getFloor(z); + if (!floor) { + return nullptr; + } + + const auto tile = floor->getTile(x, y); + return tile; +} + std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) { if (z >= MAP_MAX_LAYERS) { return nullptr; @@ -168,6 +195,19 @@ std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) { return tile ? tile : getOrCreateTileFromCache(floor, x, y); } +void Map::refreshZones(uint16_t x, uint16_t y, uint8_t z) { + const auto tile = getLoadedTile(x, y, z); + if (!tile) { + return; + } + + tile->clearZones(); + const auto &zones = Zone::getZones(tile->getPosition()); + for (const auto &zone : zones) { + tile->addZone(zone); + } +} + void Map::setTile(uint16_t x, uint16_t y, uint8_t z, std::shared_ptr newTile) { if (z >= MAP_MAX_LAYERS) { g_logger().error("Attempt to set tile on invalid coordinate: {}", Position(x, y, z).toString()); diff --git a/src/map/map.hpp b/src/map/map.hpp index 501f7578b6d..ac0211c7daf 100644 --- a/src/map/map.hpp +++ b/src/map/map.hpp @@ -50,7 +50,7 @@ class Map : protected MapCache { * \param loadNpcs if true, the main map npcs is loaded * \returns true if the main map was loaded successfully */ - void loadMap(const std::string &identifier, bool mainMap = false, bool loadHouses = false, bool loadMonsters = false, bool loadNpcs = false, const Position &pos = Position()); + void loadMap(const std::string &identifier, bool mainMap = false, bool loadHouses = false, bool loadMonsters = false, bool loadNpcs = false, bool loadZones = false, const Position &pos = Position()); /** * Load the custom map * \param identifier Is the map custom folder @@ -59,7 +59,7 @@ class Map : protected MapCache { * \param loadNpcs if true, the map custom npcs is loaded * \returns true if the custom map was loaded successfully */ - void loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, const int customMapIndex); + void loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, bool loadZones, const int customMapIndex); void loadHouseInfo(); @@ -78,6 +78,11 @@ class Map : protected MapCache { return getTile(pos.x, pos.y, pos.z); } + void refreshZones(uint16_t x, uint16_t y, uint8_t z); + void refreshZones(const Position &pos) { + refreshZones(pos.x, pos.y, pos.z); + } + std::shared_ptr getOrCreateTile(uint16_t x, uint16_t y, uint8_t z, bool isDynamic = false); std::shared_ptr getOrCreateTile(const Position &pos, bool isDynamic = false) { return getOrCreateTile(pos.x, pos.y, pos.z, isDynamic); @@ -147,11 +152,13 @@ class Map : protected MapCache { void setTile(const Position &pos, std::shared_ptr newTile) { setTile(pos.x, pos.y, pos.z, newTile); } + std::shared_ptr getLoadedTile(uint16_t x, uint16_t y, uint8_t z); std::filesystem::path path; std::string monsterfile; std::string housefile; std::string npcfile; + std::string zonesfile; uint32_t width = 0; uint32_t height = 0; diff --git a/src/map/mapcache.cpp b/src/map/mapcache.cpp index dfbbc58c3e3..c3962980698 100644 --- a/src/map/mapcache.cpp +++ b/src/map/mapcache.cpp @@ -16,6 +16,7 @@ #include "io/iologindata.hpp" #include "items/item.hpp" #include "game/game.hpp" +#include "game/zones/zone.hpp" #include "map/map.hpp" #include "utils/hash.hpp" #include "io/filestream.hpp" @@ -132,6 +133,9 @@ std::shared_ptr MapCache::getOrCreateTileFromCache(const std::unique_ptrsetFlag(static_cast(cachedTile->flags)); + for (const auto &zone : Zone::getZones(pos)) { + tile->addZone(zone); + } floor->setTile(x, y, tile); diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index ee6b8da2f2b..e84dcf58a64 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -655,6 +655,9 @@ enum ItemID_t : uint16_t { ITEM_PRIMAL_POD = 39176, ITEM_DIVINE_EMPOWERMENT = 40450, + ITEM_BATHTUB_FILLED = 26077, + ITEM_BATHTUB_FILLED_NOTMOVABLE = 26100, + ITEM_NONE = 0 };