From 0fb2250aedeb8163a1ef4d50c37579ac26e65a76 Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:23:46 -0600 Subject: [PATCH 1/4] [core] Add setting to enable semi-randomized spawn positions --- scripts/specs/core/CBaseEntity.lua | 5 +++++ settings/default/map.lua | 3 +++ src/map/battlefield.cpp | 2 ++ src/map/entities/mobentity.cpp | 21 +++++++++++++++++++++ src/map/entities/mobentity.h | 3 ++- src/map/lua/lua_baseentity.cpp | 22 ++++++++++++++++++++++ src/map/lua/lua_baseentity.h | 1 + src/map/utils/zoneutils.cpp | 4 ++++ src/map/zone.h | 3 ++- 9 files changed, 62 insertions(+), 2 deletions(-) diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index c82f19e3628..9ceb8f4890f 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -3758,6 +3758,11 @@ end function CBaseEntity:setSpawn(x, y, z, rot) end +---@param isFixed boolean +---return nil +function CBaseEntity:setFixedSpawnPosition(isFixed) +end + ---@nodiscard ---@return integer function CBaseEntity:getRespawnTime() diff --git a/settings/default/map.lua b/settings/default/map.lua index 00b256c54c3..f736f6ad5bf 100644 --- a/settings/default/map.lua +++ b/settings/default/map.lua @@ -232,6 +232,9 @@ xi.settings.map = -- Allow mobs to walk back home instead of despawning MOB_NO_DESPAWN = false, + -- Enable randomized spawn position for non-popped mobs (experimental) + MOB_RANDOMIZE_SPAWN_LOCATION = false, + -- Adds extra time to mob despawn in seconds. Base time is 25s, so a setting of 5 here would be a total of 30 seconds. MOB_ADDITIONAL_TIME_TO_DEAGGRO = 0, diff --git a/src/map/battlefield.cpp b/src/map/battlefield.cpp index 907f017a511..32184640ddd 100644 --- a/src/map/battlefield.cpp +++ b/src/map/battlefield.cpp @@ -391,6 +391,8 @@ bool CBattlefield::InsertEntity(CBaseEntity* PEntity, bool enter, BATTLEFIELDMOB m_AdditionalEnemyList.emplace_back(mob); } + mob.PMob->fixedSpawnPoint = true; // mobs spawn at fixed positions in battlefields + // todo: this can be greatly improved if (mob.PMob->isAlive()) { diff --git a/src/map/entities/mobentity.cpp b/src/map/entities/mobentity.cpp index 0b95295aa22..42f20e74850 100644 --- a/src/map/entities/mobentity.cpp +++ b/src/map/entities/mobentity.cpp @@ -43,6 +43,7 @@ #include "mob_spell_container.h" #include "mob_spell_list.h" #include "mobskill.h" +#include "navmesh.h" #include "packets/action.h" #include "packets/entity_update.h" #include "packets/pet_sync.h" @@ -134,6 +135,8 @@ CMobEntity::CMobEntity() , m_bcnmID(0) , m_giveExp(false) , m_neutral(false) +, m_SpawnPoint() +, fixedSpawnPoint(false) , m_Element(0) , m_HiPCLvl(0) , m_HiPartySize(0) @@ -662,6 +665,24 @@ void CMobEntity::Spawn() // spawn somewhere around my point loc.p = m_SpawnPoint; + if (loc.zone && loc.zone->m_navMesh && !loc.zone->loadingMobs && // If we have a navmesh and the zone isn't doing initialization (don't randomize spawns on map load) + settings::get("map.MOB_RANDOMIZE_SPAWN_LOCATION") && // check settings + !fixedSpawnPoint) // Does not have a fixed spawn position + { + float spawnRadius = 10.f; // TODO: mobmods? + + if ((loc.zone->GetTypeMask() & ZONE_TYPE::DUNGEON) == ZONE_TYPE::DUNGEON) + { + spawnRadius = 5.f; + } + + auto status = loc.zone->m_navMesh->findRandomPosition(m_SpawnPoint, spawnRadius); + if (status.first == 0) // Only try once + { + loc.p = status.second; + } + } + if (m_roamFlags & ROAMFLAG_STEALTH) { HideName(true); diff --git a/src/map/entities/mobentity.h b/src/map/entities/mobentity.h index efe7f5443da..872acf70b43 100644 --- a/src/map/entities/mobentity.h +++ b/src/map/entities/mobentity.h @@ -239,7 +239,8 @@ class CMobEntity : public CBattleEntity bool m_giveExp; // prevent exp gain bool m_neutral; // stop linking / aggroing - position_t m_SpawnPoint; // spawn point of mob + position_t m_SpawnPoint; // spawn point of mob + bool fixedSpawnPoint; // popped from QM or otherwise has a fixed position uint8 m_Element; uint8 m_HiPCLvl; // Highest Level of Player Character that hit the Monster diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 7db6008416b..a24652a4e4d 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -17344,6 +17344,27 @@ void CLuaBaseEntity::setSpawn(float x, float y, float z, const sol::object& rot) PMob->m_SpawnPoint.y = y; PMob->m_SpawnPoint.z = z; PMob->m_SpawnPoint.rotation = (rot != sol::lua_nil) ? rot.as() : 0; + PMob->fixedSpawnPoint = true; // TODO: add more functionality around this. setSpawn is primarily used for very precise spawns. +} + +/************************************************************************ + * Function: setFixedSpawnPosition() + * Purpose : Set whether or not the mob will spawn only at their spawn points in the db + * Example : mob:setFixedSpawnPosition(true) + * Notes : + ************************************************************************/ + +void CLuaBaseEntity::setFixedSpawnPosition(bool isFixed) +{ + if (m_PBaseEntity->objtype != TYPE_MOB) + { + ShowWarning("Attempting to set spawn for invalid entity type (%s).", m_PBaseEntity->getName()); + return; + } + + auto* PMob = static_cast(m_PBaseEntity); + + PMob->fixedSpawnPoint = isFixed; // TODO: add more functionality around this. } /************************************************************************ @@ -20223,6 +20244,7 @@ void CLuaBaseEntity::Register() SOL_REGISTER("isSpawned", CLuaBaseEntity::isSpawned); SOL_REGISTER("getSpawnPos", CLuaBaseEntity::getSpawnPos); SOL_REGISTER("setSpawn", CLuaBaseEntity::setSpawn); + SOL_REGISTER("setFixedSpawnPosition", CLuaBaseEntity::setFixedSpawnPosition); SOL_REGISTER("getRespawnTime", CLuaBaseEntity::getRespawnTime); SOL_REGISTER("setRespawnTime", CLuaBaseEntity::setRespawnTime); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 6dd1c2af811..cede53d1b11 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -852,6 +852,7 @@ class CLuaBaseEntity bool isSpawned(); auto getSpawnPos() -> sol::table; void setSpawn(float x, float y, float z, const sol::object& rot); + void setFixedSpawnPosition(bool isFixed); uint32 getRespawnTime(); void setRespawnTime(uint32 seconds); diff --git a/src/map/utils/zoneutils.cpp b/src/map/utils/zoneutils.cpp index 06c0fc9bc74..c126c9ad087 100644 --- a/src/map/utils/zoneutils.cpp +++ b/src/map/utils/zoneutils.cpp @@ -662,6 +662,8 @@ void LoadMOBList(const std::vector& zoneIds) // clang-format off ForEachZone(zoneIds, [](CZone* PZone) { + PZone->loadingMobs = true; + for (auto &spawnGroup : PZone->m_spawnGroups) { spawnGroup.second->fillSpawnPool(); @@ -714,6 +716,8 @@ void LoadMOBList(const std::vector& zoneIds) } } }); + + PZone->loadingMobs = false; }); // clang-format on } diff --git a/src/map/zone.h b/src/map/zone.h index c3362c146cd..278473cce4a 100644 --- a/src/map/zone.h +++ b/src/map/zone.h @@ -659,7 +659,8 @@ class CZone std::unique_ptr lineOfSight; std::map> m_spawnGroups; - timer::time_point m_LoadedAt; // The time the zone was loaded + timer::time_point m_LoadedAt; // The time the zone was loaded + bool loadingMobs; // Zone is doing initial mob load void LoadNavMesh(); void LoadZoneLos(); From 887aed68bca1ae16f4a04c5431d059ad277d0622 Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:26:40 -0600 Subject: [PATCH 2/4] [lua] set most Zdei to have fixed spawn positions --- scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei.lua | 4 ++++ scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei_Still.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Fast.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Still.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_BLM.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_RDM.lua | 4 ++++ scripts/zones/The_Garden_of_RuHmet/mobs/Qnzdei.lua | 2 ++ 8 files changed, 30 insertions(+) diff --git a/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei.lua b/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei.lua index fecf400cf99..3f742c8efec 100644 --- a/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei.lua +++ b/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei.lua @@ -7,6 +7,10 @@ mixins = { require('scripts/mixins/families/zdei') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onPath = function(mob) local spawnPos = mob:getSpawnPos() mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z }) diff --git a/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei_Still.lua b/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei_Still.lua index b423f4f40c8..237b915a715 100644 --- a/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei_Still.lua +++ b/scripts/zones/Grand_Palace_of_HuXzoi/mobs/Eozdei_Still.lua @@ -8,6 +8,10 @@ mixins = { require('scripts/mixins/families/zdei') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onPath = function(mob) local spawnPos = mob:getSpawnPos() mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z }) diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei.lua index 2379ca31675..4e1c6595151 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei.lua @@ -7,6 +7,10 @@ mixins = { require('scripts/mixins/families/zdei') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onPath = function(mob) local spawnPos = mob:getSpawnPos() mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z }) diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Fast.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Fast.lua index 2379ca31675..4e1c6595151 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Fast.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Fast.lua @@ -7,6 +7,10 @@ mixins = { require('scripts/mixins/families/zdei') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onPath = function(mob) local spawnPos = mob:getSpawnPos() mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z }) diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Still.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Still.lua index 299b81f5f76..d9307987894 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Still.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Awzdei_Still.lua @@ -7,6 +7,10 @@ mixins = { require('scripts/mixins/families/zdei') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onPath = function(mob) local spawnPos = mob:getSpawnPos() mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z }) diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_BLM.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_BLM.lua index dc7f1c5a098..4f145b100d8 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_BLM.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_BLM.lua @@ -9,6 +9,10 @@ mixins = { require('scripts/mixins/job_special') } ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + local chargeOptic = function(mob) mob:setAutoAttackEnabled(false) mob:setMobAbilityEnabled(false) diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_RDM.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_RDM.lua index eb2da85486e..4adeae96c67 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_RDM.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Ixzdei_RDM.lua @@ -25,6 +25,10 @@ local chargeOptic = function(mob) end end +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onMobSpawn = function(mob) xi.mix.jobSpecial.config(mob, { specials = diff --git a/scripts/zones/The_Garden_of_RuHmet/mobs/Qnzdei.lua b/scripts/zones/The_Garden_of_RuHmet/mobs/Qnzdei.lua index 07341d2c4ed..1b2da0ccc8f 100644 --- a/scripts/zones/The_Garden_of_RuHmet/mobs/Qnzdei.lua +++ b/scripts/zones/The_Garden_of_RuHmet/mobs/Qnzdei.lua @@ -34,6 +34,8 @@ entity.onMobInitialize = function(mob) if subLinkValue then mob:setMobMod(xi.mobMod.SUBLINK, subLinkValue) end + + mob:setFixedSpawnPosition(true) end local changeState = function(mob, idle) From 1d48ff9d713a25e2865784e8fa6adb1b3d0e782d Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:27:12 -0600 Subject: [PATCH 3/4] [lua] Set Eschan Gargouille and Groundskeeper to have fixed spawns --- scripts/zones/Escha_RuAun/mobs/Eschan_Gargouille.lua | 4 ++++ scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/scripts/zones/Escha_RuAun/mobs/Eschan_Gargouille.lua b/scripts/zones/Escha_RuAun/mobs/Eschan_Gargouille.lua index ef1f7c37834..9ebe5cab872 100644 --- a/scripts/zones/Escha_RuAun/mobs/Eschan_Gargouille.lua +++ b/scripts/zones/Escha_RuAun/mobs/Eschan_Gargouille.lua @@ -5,6 +5,10 @@ ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) +end + entity.onMobSpawn = function(mob) mob:hideName(true) mob:setUntargetable(true) diff --git a/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua b/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua index e36ae3e5933..25147cb3e8d 100644 --- a/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua +++ b/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua @@ -8,6 +8,10 @@ local ID = zones[xi.zone.RUAUN_GARDENS] ---@type TMobEntity local entity = {} +entity.onMobInitialize = function(mob) + mob:setFixedSpawnPosition(true) -- TODO: change this to only apply to "wall" groundskeepers +end + entity.onMobDeath = function(mob, player, optParams) xi.regime.checkRegime(player, mob, 143, 2, xi.regime.type.FIELDS) xi.regime.checkRegime(player, mob, 144, 1, xi.regime.type.FIELDS) From 47a68345109bb3f09bc54a8657ae5af581ba9d4c Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:36:47 -0600 Subject: [PATCH 4/4] [lua] Set fixed spawn position for colonization reive mobs --- scripts/globals/colonization_reives.lua | 2 ++ scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/globals/colonization_reives.lua b/scripts/globals/colonization_reives.lua index d7a0c2ae03f..c5ddf12daf3 100644 --- a/scripts/globals/colonization_reives.lua +++ b/scripts/globals/colonization_reives.lua @@ -145,6 +145,8 @@ xi.reives.enableReive = function(zoneID, reiveNum) for _, entryId in pairs(reiveData.mob) do local mob = GetMobByID(entryId) if mob then + mob:setFixedSpawnPosition(true) + if not mob:isAlive() then SpawnMob(entryId) -- Spawn the reive defenders -- TODO: Set name flags (sword) diff --git a/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua b/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua index 25147cb3e8d..c8f9012e0bd 100644 --- a/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua +++ b/scripts/zones/RuAun_Gardens/mobs/Groundskeeper.lua @@ -24,10 +24,14 @@ entity.onMobDespawn = function(mob) local params = {} params.immediate = true if xi.mob.phOnDespawn(mob, ID.mob.DESPOT, 5, 7200, params) then -- 2 hours - local phId = mob:getID() - GetMobByID(ID.mob.DESPOT):addListener('SPAWN', 'PH_VAR', function(m) - m:setLocalVar('ph', phId) - end) + local phId = mob:getID() + local despot = GetMobByID(ID.mob.DESPOT) + + if despot then + despot:addListener('SPAWN', 'PH_VAR', function(m) + m:setLocalVar('ph', phId) + end) + end end end