From eeab5f408d95449db3f35d37d8606a7616165b80 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:34:58 -0300 Subject: [PATCH] fix(Core/Maps): keep bosses of DONE encounters down on the dynamic respawn path (#27788) Co-authored-by: Gultask <100873791+Gultask@users.noreply.github.com> --- .../game/AI/ScriptedAI/ScriptedCreature.cpp | 4 +++ .../game/Entities/Creature/Creature.cpp | 9 +++++-- src/server/game/Instances/InstanceSaveMgr.cpp | 4 ++- src/server/game/Instances/InstanceScript.cpp | 6 +++++ src/server/game/Instances/InstanceScript.h | 4 +++ src/server/game/Maps/Map.cpp | 27 ++++++++++++++++--- src/server/game/Maps/Map.h | 3 +++ src/server/scripts/Commands/cs_misc.cpp | 8 ++---- 8 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp b/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp index 62467c5175..9a9a407948 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp @@ -618,7 +618,11 @@ BossAI::BossAI(Creature* creature, uint32 bossId) : ScriptedAI(creature), { callForHelpRange = 0.0f; if (instance) + { SetBoundary(instance->GetBossBoundary(bossId)); + if (creature->GetSpawnId()) + instance->RegisterBossSpawn(creature->GetSpawnId(), bossId); + } // Prevents updating the scheduler's timer while the creature is casting. // Clear it in the script if you need it to update while the creature is casting. diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 8f84bd3351..2182bd8d02 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -2154,8 +2154,13 @@ void Creature::Respawn(bool force) if (m_spawnId) { // Set respawn time to now so ProcessRespawns() picks it up - time_t now = GameTime::GetGameTime().count(); - GetMap()->SaveCreatureRespawnTime(m_spawnId, now); + if (force) + GetMap()->ForceCreatureRespawn(m_spawnId); + else + { + time_t now = GameTime::GetGameTime().count(); + GetMap()->SaveCreatureRespawnTime(m_spawnId, now); + } } AddObjectToRemoveList(); } diff --git a/src/server/game/Instances/InstanceSaveMgr.cpp b/src/server/game/Instances/InstanceSaveMgr.cpp index 14ce15f25d..6cc5048c40 100644 --- a/src/server/game/Instances/InstanceSaveMgr.cpp +++ b/src/server/game/Instances/InstanceSaveMgr.cpp @@ -360,10 +360,12 @@ void InstanceSaveMgr::LoadResetTimes() // calculate the next reset time t = (t / DAY) * DAY; t += ((today - t) / period + 1) * period + diff; + SetResetTimeFor(mapid, difficulty, t); CharacterDatabase.DirectExecute("UPDATE instance_reset SET resettime = '{}' WHERE mapid = '{}' AND difficulty = '{}'", (uint32)t, mapid, difficulty); } - SetExtendedResetTimeFor(mapid, difficulty, t); + // An extended lock runs one period past the reset, as _ResetOrWarnAll sets it after a runtime reset + SetExtendedResetTimeFor(mapid, difficulty, t + period); // schedule the global reset/warning uint8 type; diff --git a/src/server/game/Instances/InstanceScript.cpp b/src/server/game/Instances/InstanceScript.cpp index cf4d56aa0f..16b336ca91 100644 --- a/src/server/game/Instances/InstanceScript.cpp +++ b/src/server/game/Instances/InstanceScript.cpp @@ -87,6 +87,12 @@ void InstanceScript::OnCreatureRemove(Creature* creature) RemoveMinion(creature); } +bool InstanceScript::IsBossSpawnDone(ObjectGuid::LowType spawnId) const +{ + auto itr = _bossSpawns.find(spawnId); + return itr != _bossSpawns.end() && GetBossState(itr->second) == DONE; +} + void InstanceScript::OnGameObjectCreate(GameObject* go) { AddObject(go); diff --git a/src/server/game/Instances/InstanceScript.h b/src/server/game/Instances/InstanceScript.h index 8b9c1e6779..f97999d7cb 100644 --- a/src/server/game/Instances/InstanceScript.h +++ b/src/server/game/Instances/InstanceScript.h @@ -250,6 +250,9 @@ public: virtual bool SetBossState(uint32 id, EncounterState state); EncounterState GetBossState(uint32 id) const { return id < bosses.size() ? bosses[id].state : TO_BE_DECIDED; } + // Spawns a BossAI bound to an encounter; the respawn system keeps them down while it is DONE + void RegisterBossSpawn(ObjectGuid::LowType spawnId, uint32 bossId) { _bossSpawns[spawnId] = bossId; } + bool IsBossSpawnDone(ObjectGuid::LowType spawnId) const; static std::string GetBossStateName(uint8 state); CreatureBoundary const* GetBossBoundary(uint32 id) const { return id < bosses.size() ? &bosses[id].boundary : nullptr; } BossInfo const* GetBossInfo(uint32 id) const { return &bosses[id]; } @@ -351,6 +354,7 @@ private: DoorInfoMap doors; MinionInfoMap minions; ObjectInfoMap _creatureInfo; + std::unordered_map _bossSpawns; ObjectInfoMap _gameObjectInfo; ObjectInfoMap _summonInfo; ObjectGuidMap _objectGuids; diff --git a/src/server/game/Maps/Map.cpp b/src/server/game/Maps/Map.cpp index 86b86a2729..ba4c57ed8e 100644 --- a/src/server/game/Maps/Map.cpp +++ b/src/server/game/Maps/Map.cpp @@ -2519,6 +2519,13 @@ void Map::RemoveCreatureRespawnTime(ObjectGuid::LowType spawnId) CharacterDatabase.Execute(stmt); } +void Map::ForceCreatureRespawn(ObjectGuid::LowType spawnId) +{ + time_t now = GameTime::GetGameTime().count(); + SaveCreatureRespawnTime(spawnId, now); + _forcedCreatureRespawns.insert(spawnId); +} + void Map::SaveGORespawnTime(ObjectGuid::LowType spawnId, time_t& respawnTime) { if (!respawnTime) @@ -2810,6 +2817,9 @@ void Map::ProcessRespawns() void Map::ProcessCreatureRespawn(ObjectGuid::LowType spawnId) { + // Consumed by every attempt, or a mark set on a continent would never go away + bool const forced = _forcedCreatureRespawns.erase(spawnId) != 0; + // Pool members are handled entirely by the pool system on this map's pool data if (uint32 poolId = sPoolMgr->IsPartOfAPool(spawnId)) { @@ -2860,14 +2870,25 @@ void Map::ProcessCreatureRespawn(ObjectGuid::LowType spawnId) } } + // A spawn whose BossAI bound it to a DONE encounter stays down, as BossAI::CanRespawn keeps + // compat-mode spawns down. The expired row is kept so a reload still loads the boss dead, and + // the check repeats like an inactive group's. A forced Respawn() passes, as force does in compat mode. + if (InstanceMap* instanceMap = ToInstanceMap()) + if (InstanceScript* script = instanceMap->GetInstanceScript()) + if (!forced && script->IsBossSpawnDone(spawnId)) + { + _respawnQueue.insert({GameTime::GetGameTime().count() + 5, SPAWN_TYPE_CREATURE, spawnId}); + return; + } + // Check linked_respawn: don't spawn if the master creature is still dead. // This mirrors the check in Creature::Respawn() for compat-mode creatures: - // hard-reset creatures bypass it (they despawn on evade and must always - // come back), and a creature linked to itself never auto-respawns. + // hard-reset creatures and forced respawns bypass it (the former despawn on + // evade and must always come back), and a creature linked to itself never auto-respawns. ObjectGuid dbtableHighGuid = ObjectGuid::Create(data->id, spawnId); time_t linkedRespawntime = GetLinkedRespawnTime(dbtableHighGuid); CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(data->id); - if (linkedRespawntime && !(cInfo && cInfo->HasFlagsExtra(CREATURE_FLAG_EXTRA_HARD_RESET))) + if (linkedRespawntime && !forced && !(cInfo && cInfo->HasFlagsExtra(CREATURE_FLAG_EXTRA_HARD_RESET))) { time_t now = GameTime::GetGameTime().count(); time_t newRespawnTime; diff --git a/src/server/game/Maps/Map.h b/src/server/game/Maps/Map.h index 6eb3d4de6d..c74bab8363 100644 --- a/src/server/game/Maps/Map.h +++ b/src/server/game/Maps/Map.h @@ -433,6 +433,8 @@ public: void SaveCreatureRespawnTime(ObjectGuid::LowType dbGuid, time_t& respawnTime); void RemoveCreatureRespawnTime(ObjectGuid::LowType dbGuid); + // Queues the spawn for the next respawn pass and lets it through the DONE-boss hold, as force bypasses CanRespawn in compat mode + void ForceCreatureRespawn(ObjectGuid::LowType dbGuid); void SaveGORespawnTime(ObjectGuid::LowType dbGuid, time_t& respawnTime); void RemoveGORespawnTime(ObjectGuid::LowType dbGuid); [[nodiscard]] std::unordered_map const& GetCreatureRespawnTimes() const { return _creatureRespawnTimes; } @@ -649,6 +651,7 @@ private: void _RemoveObjectFromUpdateList(WorldObject* obj); std::unordered_map _creatureRespawnTimes; + std::unordered_set _forcedCreatureRespawns; std::unordered_map _goRespawnTimes; // Time-ordered index for ProcessRespawns() — avoids O(n) full scan. diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index 2598fbf75b..5cfc67f586 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -2563,10 +2563,7 @@ public: } // Also trigger via respawn time queue for fully-removed spawns if (map->GetCreatureRespawnTime(spawnId) > 0) - { - time_t now = GameTime::GetGameTime().count(); - map->SaveCreatureRespawnTime(spawnId, now); - } + map->ForceCreatureRespawn(spawnId); handler->PSendSysMessage(LANG_RESPAWN_GUID_CREATURE_QUEUED, spawnId, creData->id); return true; } @@ -2658,7 +2655,6 @@ public: return false; } - time_t now = GameTime::GetGameTime().count(); uint32 count = 0; // Phase 1: respawn dead corpses that are still tracked in the spawn-id store. @@ -2691,7 +2687,7 @@ public: } for (ObjectGuid::LowType spawnId : toRespawn) { - map->SaveCreatureRespawnTime(spawnId, now); + map->ForceCreatureRespawn(spawnId); ++count; }