From d3865d613bd34b67a70a558ce13d6b41ebf5e17c Mon Sep 17 00:00:00 2001 From: sruon Date: Sat, 14 Feb 2026 08:08:08 -0700 Subject: [PATCH 1/2] Set allow respawn for scripted mobs setting timer in initialize --- .../mobs/Noble_Mold.lua | 2 +- scripts/zones/Xarcabard/mobs/Ereshkigal.lua | 2 +- src/map/lua/lua_baseentity.cpp | 11 +++---- src/map/lua/lua_baseentity.h | 12 ++++---- src/map/spawn_handler.cpp | 29 +++++++++++++++++++ src/map/spawn_handler.h | 1 + src/map/utils/zoneutils.cpp | 4 +++ 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/scripts/zones/The_Sanctuary_of_ZiTah/mobs/Noble_Mold.lua b/scripts/zones/The_Sanctuary_of_ZiTah/mobs/Noble_Mold.lua index 9f3b3ab3441..5c0c3534da6 100644 --- a/scripts/zones/The_Sanctuary_of_ZiTah/mobs/Noble_Mold.lua +++ b/scripts/zones/The_Sanctuary_of_ZiTah/mobs/Noble_Mold.lua @@ -28,7 +28,7 @@ entity.onMobDespawn = function(mob) local ph = GetMobByID(mob:getID() - 1) if ph then DisallowRespawn(ph:getID(), false) - ph:setRespawnTime(ph:getRespawnTime()) + ph:setRespawnTime(GetMobRespawnTime(ph:getID())) end end diff --git a/scripts/zones/Xarcabard/mobs/Ereshkigal.lua b/scripts/zones/Xarcabard/mobs/Ereshkigal.lua index bb8d1aafed6..1f769408a0a 100644 --- a/scripts/zones/Xarcabard/mobs/Ereshkigal.lua +++ b/scripts/zones/Xarcabard/mobs/Ereshkigal.lua @@ -37,7 +37,7 @@ entity.onMobDespawn = function(mob) local ph = GetMobByID(mob:getID() - 1) if ph then DisallowRespawn(ph:getID(), false) - ph:setRespawnTime(ph:getRespawnTime()) + ph:setRespawnTime(GetMobRespawnTime(ph:getID())) end end diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 100a76f3699..ff03893bd6a 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -17506,7 +17506,7 @@ void CLuaBaseEntity::setSpawn(float x, float y, float z, const sol::object& rot) * Notes : Used in mobs.lua...and directly in Charybdis ************************************************************************/ -uint32 CLuaBaseEntity::getRespawnTime() +auto CLuaBaseEntity::getRespawnTime() const -> uint32 { if (m_PBaseEntity->objtype != TYPE_MOB) { @@ -17514,11 +17514,12 @@ uint32 CLuaBaseEntity::getRespawnTime() return 0; } - CMobEntity* PMob = static_cast(m_PBaseEntity); - - if (PMob->m_AllowRespawn) + if (auto* PMob = static_cast(m_PBaseEntity); PMob->loc.zone) { - return static_cast(timer::count_seconds(PMob->m_RespawnTime)); + if (const auto remaining = PMob->loc.zone->spawnHandler()->getRemainingRespawnTime(PMob)) + { + return static_cast(timer::count_seconds(*remaining)); + } } return 0; diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index d00d661627e..c2a99ad8fc7 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -852,12 +852,12 @@ class CLuaBaseEntity void setNpcFlags(uint32 flags); - void spawn(const sol::object& despawnSec, const sol::object& respawnSec); - bool isSpawned(); - auto getSpawnPos() -> sol::table; - void setSpawn(float x, float y, float z, const sol::object& rot); - uint32 getRespawnTime(); - void setRespawnTime(uint32 seconds) const; + void spawn(const sol::object& despawnSec, const sol::object& respawnSec); + bool isSpawned(); + auto getSpawnPos() -> sol::table; + void setSpawn(float x, float y, float z, const sol::object& rot); + auto getRespawnTime() const -> uint32; + void setRespawnTime(uint32 seconds) const; void instantiateMob(uint32 groupID); diff --git a/src/map/spawn_handler.cpp b/src/map/spawn_handler.cpp index bbcd5e1c827..4bdc378f58b 100644 --- a/src/map/spawn_handler.cpp +++ b/src/map/spawn_handler.cpp @@ -73,6 +73,35 @@ auto SpawnHandler::isRegistered(CMobEntity* PMob) const -> bool return pendingRespawns_.contains(PMob->id); } +auto SpawnHandler::getRemainingRespawnTime(CMobEntity* PMob) const -> std::optional +{ + if (!PMob) + { + return std::nullopt; + } + + const auto now = timer::now(); + + if (SpawnSlot* slot = PMob->GetSpawnSlot()) + { + if (auto it = pendingSlotRespawns_.find(slot); it != pendingSlotRespawns_.end()) + { + const auto remaining = it->second.respawnAt - now; + return remaining > timer::duration::zero() ? remaining : timer::duration::zero(); + } + } + else + { + if (auto it = pendingRespawns_.find(PMob->id); it != pendingRespawns_.end()) + { + const auto remaining = it->second - now; + return remaining > timer::duration::zero() ? remaining : timer::duration::zero(); + } + } + + return std::nullopt; +} + // Every 30 seconds, attempt to spawn any mob pending respawn. // Mobs are respawned if: // - Their respawn timer is due within the next 15s (half interval) diff --git a/src/map/spawn_handler.h b/src/map/spawn_handler.h index a31905ee3d7..8a93539aa69 100644 --- a/src/map/spawn_handler.h +++ b/src/map/spawn_handler.h @@ -49,6 +49,7 @@ class SpawnHandler void Tick(timer::time_point now); void registerForRespawn(CMobEntity* PMob, std::optional respawnTime = std::nullopt); auto isRegistered(CMobEntity* PMob) const -> bool; + auto getRemainingRespawnTime(CMobEntity* PMob) const -> std::optional; void onTOTDChange(vanadiel_time::TOTD totd) const; void onWeatherChange(Weather weather) const; auto canSpawnNow(const CMobEntity* PMob) const -> bool; diff --git a/src/map/utils/zoneutils.cpp b/src/map/utils/zoneutils.cpp index ec61d6113be..11b90de3958 100644 --- a/src/map/utils/zoneutils.cpp +++ b/src/map/utils/zoneutils.cpp @@ -686,6 +686,10 @@ void LoadMOBList(const std::vector& zoneIds) // Skip mobs already registered via setRespawnTime in onMobInitialize - let SpawnHandler handle them if (PZone->spawnHandler()->isRegistered(PMob)) { + if (PMob->m_SpawnType == SPAWNTYPE_SCRIPTED && PMob->m_RespawnTime > 0s) + { + PMob->m_AllowRespawn = true; + } return; } From d1db4d7ef20c1e09d972f43fba5cbf3cb4f078b8 Mon Sep 17 00:00:00 2001 From: sruon Date: Sat, 14 Feb 2026 08:34:12 -0700 Subject: [PATCH 2/2] setRespawnTime(0) disables respawn --- scripts/tests/systems/spawn_handler.lua | 68 +++++++++++++++++++++++++ src/map/lua/lua_baseentity.cpp | 20 +++++--- src/map/spawn_handler.cpp | 28 +++++++++- src/map/spawn_handler.h | 1 + 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/scripts/tests/systems/spawn_handler.lua b/scripts/tests/systems/spawn_handler.lua index c640c5b19a9..7abe2e428f5 100644 --- a/scripts/tests/systems/spawn_handler.lua +++ b/scripts/tests/systems/spawn_handler.lua @@ -279,6 +279,74 @@ describe('Spawn Handler', function() end) end) + describe('scripted spawns', function() + it('respawns Simurgh after kill within respawn window', function() + player:gotoZone(xi.zone.ROLANBERRY_FIELDS) + local ID = zones[xi.zone.ROLANBERRY_FIELDS] + local simurgh = player.entities:get(ID.mob.SIMURGH) + + -- Initial spawn + xi.test.world:skipTime(7200) + xi.test.world:tick(xi.tick.SPAWN) + simurgh.assert:isSpawned() + + player:claimAndKillMob(simurgh) + + -- Skip 2 hours again for respawn + xi.test.world:skipTime(7200) + xi.test.world:tick(xi.tick.SPAWN) + + simurgh.assert:isSpawned() + end) + + it('King Arthro spawns when all Knight Crabs killed and blocks their respawn', function() + player:gotoZone(xi.zone.JUGNER_FOREST) + local ID = zones[xi.zone.JUGNER_FOREST] + local kingArthro = player.entities:get(ID.mob.KING_ARTHRO) + + -- Gather all 10 Knight Crabs and wait for initial spawn + local knightCrabs = {} + for offset = 1, 10 do + knightCrabs[offset] = player.entities:get(ID.mob.KING_ARTHRO - offset) + end + + xi.test.world:skipTime(12000) + xi.test.world:tick(xi.tick.SPAWN) + + for _, crab in ipairs(knightCrabs) do + crab.assert:isSpawned() + end + + -- Kill all 10 Knight Crabs + for _, crab in ipairs(knightCrabs) do + player:claimAndKillMob(crab) + end + + -- King Arthro should spawn + xi.test.world:tick() + kingArthro.assert:isSpawned() + + -- Let KA roam for some time - Knight Crabs should NOT respawn + xi.test.world:skipTime(3600) + xi.test.world:tick(xi.tick.SPAWN) + for _, crab in ipairs(knightCrabs) do + crab.assert.no:isSpawned() + end + + -- Kill King Arthro + player:claimAndKillMob(kingArthro) + + -- Skip 24 hours + xi.test.world:skipTime(86400) + xi.test.world:tick(xi.tick.SPAWN) + + -- Knight Crabs should now be respawned + for _, crab in ipairs(knightCrabs) do + crab.assert:isSpawned() + end + end) + end) + -- Nunyenunc NM and Carrion Crow PH in West Sarutabaruta describe('placeholder to NM', function() it('spawns NM when lottery succeeds', function() diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index ff03893bd6a..12f15717553 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -17529,13 +17529,7 @@ auto CLuaBaseEntity::getRespawnTime() const -> uint32 * Function: setRespawnTime() * Purpose : Setting the respawn time for a Mob * Example : mob:setRespawnTime(math.random(3600, 7200)) - * Notes : Haven't seen the second argument option being used - * - * Note: Removed optional (and unused) parameter 2: - * if (!lua_isnil(L, 2) && lua_isboolean(L, 2) && lua_toboolean(L, 2)) - { // set optional parameter to true to only modify the timer - return 0; - } + * Notes : 0 disables respawn. ************************************************************************/ void CLuaBaseEntity::setRespawnTime(const uint32 seconds) const @@ -17547,6 +17541,18 @@ void CLuaBaseEntity::setRespawnTime(const uint32 seconds) const } auto* PMob = static_cast(m_PBaseEntity); + if (seconds == 0) + { + PMob->m_RespawnTime = 0s; + PMob->m_AllowRespawn = false; + + if (PMob->loc.zone != nullptr) + { + PMob->loc.zone->spawnHandler()->unregister(PMob); + } + + return; + } PMob->m_RespawnTime = std::chrono::seconds(seconds); PMob->m_AllowRespawn = true; diff --git a/src/map/spawn_handler.cpp b/src/map/spawn_handler.cpp index 4bdc378f58b..b9cb3a02e50 100644 --- a/src/map/spawn_handler.cpp +++ b/src/map/spawn_handler.cpp @@ -21,6 +21,8 @@ #include "spawn_handler.h" +#include + #include "common/timer.h" #include "common/vana_time.h" #include "entities/mobentity.h" @@ -58,6 +60,23 @@ void SpawnHandler::registerForRespawn(CMobEntity* PMob, const std::optional