diff --git a/data/sql/updates/pending_db_auth/respawn_id_rbac.sql b/data/sql/updates/pending_db_auth/respawn_id_rbac.sql new file mode 100644 index 0000000000..d77b26f8cd --- /dev/null +++ b/data/sql/updates/pending_db_auth/respawn_id_rbac.sql @@ -0,0 +1,17 @@ +-- +-- RBAC permissions for `respawn creature guid`, `respawn gameobject guid`, +-- `respawn creature entry`, and `respawn gameobject entry` commands (Console::Yes/No, security level 3 - GM) +-- +DELETE FROM `rbac_permissions` WHERE `id` IN (916, 917, 918, 919); +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(916, 'Command: respawn creature guid'), +(917, 'Command: respawn gameobject guid'), +(918, 'Command: respawn creature entry'), +(919, 'Command: respawn gameobject entry'); + +DELETE FROM `rbac_linked_permissions` WHERE `linkedId` IN (916, 917, 918, 919); +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(197, 916), +(197, 917), +(197, 918), +(197, 919); diff --git a/data/sql/updates/pending_db_world/respawn_id_and_list_respawns_strings.sql b/data/sql/updates/pending_db_world/respawn_id_and_list_respawns_strings.sql new file mode 100644 index 0000000000..7dccec609e --- /dev/null +++ b/data/sql/updates/pending_db_world/respawn_id_and_list_respawns_strings.sql @@ -0,0 +1,36 @@ +-- +-- Strings for `respawn creature guid` / `respawn gameobject guid` / +-- `respawn creature entry` / `respawn gameobject entry` commands and updated `list respawns` command +-- +DELETE FROM `acore_string` WHERE `entry` IN (35441, 35442, 35443, 35444, 35445, 35446, 35447, 35448, 35449, 35450, 35451, 35452); +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35441, 'No creature with spawn GUID {} found.', '스폰 GUID {}를 가진 생물을 찾을 수 없습니다.', 'Aucune créature avec le GUID d''apparition {} trouvée.', 'Keine Kreatur mit Spawn-GUID {} gefunden.', '未找到生成 GUID 为 {} 的生物。', '未找到生成 GUID 為 {} 的生物。', 'No se encontró ninguna criatura con el GUID de generación {}.', 'No se encontró ninguna criatura con el GUID de generación {}.', 'Существо с GUID спавна {} не найдено.'), +(35442, 'Map {} is not currently loaded.', '맵 {}이(가) 현재 로드되어 있지 않습니다.', 'La carte {} n''est pas actuellement chargée.', 'Karte {} ist derzeit nicht geladen.', '地图 {} 当前未加载。', '地圖 {} 目前未載入。', 'El mapa {} no está cargado actualmente.', 'El mapa {} no está cargado actualmente.', 'Карта {} в данный момент не загружена.'), +(35443, 'Creature (spawn GUID {}, entry {}) is already alive.', '생물 (스폰 GUID {}, 항목 {})이(가) 이미 살아 있습니다.', 'La créature (GUID d''apparition {}, entrée {}) est déjà en vie.', 'Kreatur (Spawn-GUID {}, Eintrag {}) ist bereits am Leben.', '生物(生成 GUID {},条目 {})已经存活。', '生物(生成 GUID {},條目 {})已經存活。', 'La criatura (GUID de generación {}, entrada {}) ya está viva.', 'La criatura (GUID de generación {}, entrada {}) ya está viva.', 'Существо (GUID спавна {}, запись {}) уже живо.'), +(35444, 'Creature (spawn GUID {}, entry {}) queued for respawn.', '생물 (스폰 GUID {}, 항목 {})이(가) 리스폰 대기열에 추가되었습니다.', 'La créature (GUID d''apparition {}, entrée {}) est mise en file d''attente pour réapparition.', 'Kreatur (Spawn-GUID {}, Eintrag {}) für Respawn in Warteschlange.', '生物(生成 GUID {},条目 {})已加入重生队列。', '生物(生成 GUID {},條目 {})已加入重生佇列。', 'La criatura (GUID de generación {}, entrada {}) está en cola para reaparecer.', 'La criatura (GUID de generación {}, entrada {}) está en cola para reaparecer.', 'Существо (GUID спавна {}, запись {}) добавлено в очередь на респаун.'), +(35445, 'Gameobject (spawn GUID {}, entry {}) is already active.', '게임오브젝트 (스폰 GUID {}, 항목 {})이(가) 이미 활성화되어 있습니다.', 'L''objet de jeu (GUID d''apparition {}, entrée {}) est déjà actif.', 'Spielobjekt (Spawn-GUID {}, Eintrag {}) ist bereits aktiv.', '游戏对象(生成 GUID {},条目 {})已经激活。', '遊戲物件(生成 GUID {},條目 {})已經激活。', 'El objeto de juego (GUID de generación {}, entrada {}) ya está activo.', 'El objeto de juego (GUID de generación {}, entrada {}) ya está activo.', 'Игровой объект (GUID спавна {}, запись {}) уже активен.'), +(35446, 'Gameobject (spawn GUID {}, entry {}) queued for respawn.', '게임오브젝트 (스폰 GUID {}, 항목 {})이(가) 리스폰 대기열에 추가되었습니다.', 'L''objet de jeu (GUID d''apparition {}, entrée {}) est mis en file d''attente pour réapparition.', 'Spielobjekt (Spawn-GUID {}, Eintrag {}) für Respawn in Warteschlange.', '游戏对象(生成 GUID {},条目 {})已加入重生队列。', '遊戲物件(生成 GUID {},條目 {})已加入重生佇列。', 'El objeto de juego (GUID de generación {}, entrada {}) está en cola para reaparecer.', 'El objeto de juego (GUID de generación {}, entrada {}) está en cola para reaparecer.', 'Игровой объект (GUID спавна {}, запись {}) добавлен в очередь на респаун.'), +(35447, 'A map ID is required when using this command from the console.', '콘솔에서 이 명령을 사용하려면 맵 ID가 필요합니다.', 'Un ID de carte est requis lors de l''utilisation de cette commande depuis la console.', 'Eine Karten-ID ist erforderlich, wenn dieser Befehl über die Konsole verwendet wird.', '从控制台使用此命令时需要提供地图 ID。', '從控制台使用此命令時需要提供地圖 ID。', 'Se requiere un ID de mapa al usar este comando desde la consola.', 'Se requiere un ID de mapa al usar este comando desde la consola.', 'При использовании этой команды из консоли требуется ID карты.'), +(35448, 'No gameobject with spawn GUID {} found.', '스폰 GUID {}를 가진 게임오브젝트를 찾을 수 없습니다.', 'Aucun objet de jeu avec le GUID d''apparition {} trouvé.', 'Kein Spielobjekt mit Spawn-GUID {} gefunden.', '未找到生成 GUID 为 {} 的游戏对象。', '未找到生成 GUID 為 {} 的遊戲物件。', 'No se encontró ningún objeto de juego con el GUID de generación {}.', 'No se encontró ningún objeto de juego con el GUID de generación {}.', 'Игровой объект с GUID спавна {} не найден.'), +(35449, 'No creature template with entry {} found.', '항목 {}을(를) 가진 생물 템플릿을 찾을 수 없습니다.', 'Aucun modèle de créature avec l''entrée {} trouvé.', 'Kein Kreaturvorlage mit Eintrag {} gefunden.', '未找到条目为 {} 的生物模板。', '未找到條目為 {} 的生物模板。', 'No se encontró ninguna plantilla de criatura con la entrada {}.', 'No se encontró ninguna plantilla de criatura con la entrada {}.', 'Шаблон существа с записью {} не найден.'), +(35450, 'No gameobject template with entry {} found.', '항목 {}을(를) 가진 게임오브젝트 템플릿을 찾을 수 없습니다.', 'Aucun modèle d''objet de jeu avec l''entrée {} trouvé.', 'Kein Spielobjektvorlage mit Eintrag {} gefunden.', '未找到条目为 {} 的游戏对象模板。', '未找到條目為 {} 的遊戲物件模板。', 'No se encontró ninguna plantilla de objeto de juego con la entrada {}.', 'No se encontró ninguna plantilla de objeto de juego con la entrada {}.', 'Шаблон игрового объекта с записью {} не найден.'), +(35451, 'Queued {} creature spawn(s) with entry {} for respawn on this map.', '이 맵에서 항목 {}을(를) 가진 생물 스폰 {}개가 리스폰 대기열에 추가되었습니다.', '{} apparition(s) de créature avec l''entrée {} mise(s) en file d''attente pour réapparition sur cette carte.', '{} Kreaturspawn(s) mit Eintrag {} für Respawn auf dieser Karte in Warteschlange.', '在此地图上,已将 {} 个条目为 {} 的生物生成加入重生队列。', '在此地圖上,已將 {} 個條目為 {} 的生物生成加入重生佇列。', 'Se pusieron en cola {} aparición(es) de criatura con entrada {} para reaparecer en este mapa.', 'Se pusieron en cola {} aparición(es) de criatura con entrada {} para reaparecer en este mapa.', 'В очередь на респаун на этой карте добавлено {} спавн(ов) существа с записью {}.'), +(35452, 'Queued {} gameobject spawn(s) with entry {} for respawn on this map.', '이 맵에서 항목 {}을(를) 가진 게임오브젝트 스폰 {}개가 리스폰 대기열에 추가되었습니다.', '{} apparition(s) d''objet de jeu avec l''entrée {} mise(s) en file d''attente pour réapparition sur cette carte.', '{} Spielobjektspawn(s) mit Eintrag {} für Respawn auf dieser Karte in Warteschlange.', '在此地图上,已将 {} 个条目为 {} 的游戏对象生成加入重生队列。', '在此地圖上,已將 {} 個條目為 {} 的遊戲物件生成加入重生佇列。', 'Se pusieron en cola {} aparición(es) de objeto de juego con entrada {} para reaparecer en este mapa.', 'Se pusieron en cola {} aparición(es) de objeto de juego con entrada {} para reaparecer en este mapa.', 'В очередь на респаун на этой карте добавлено {} спавн(ов) игрового объекта с записью {}.'); + +-- +-- Update `list respawns` command help text +-- +DELETE FROM `command` WHERE `name` = 'list respawns'; +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('list respawns', 2, 'Syntax: .list respawns [entryId]\r\nIn-game: shows all pending creature and gameobject respawns on the current map, filtered by entryId if provided.\r\nConsole: .list respawns #mapId [#instanceId [#entryId]] - specify map and optional instance ID and entry filter.'); + +-- +-- Register `respawn creature guid`, `respawn gameobject guid`, +-- `respawn creature entry`, and `respawn gameobject entry` commands +-- +DELETE FROM `command` WHERE `name` IN ('respawn id', 'respawn guid', 'respawn creature guid', 'respawn gameobject guid', 'respawn creature entry', 'respawn gameobject entry'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('respawn creature guid', 3, 'Syntax: .respawn creature guid #spawnGuid\r\nForces the creature with the given spawn GUID (database GUID) to respawn. Usable from console.'), +('respawn gameobject guid', 3, 'Syntax: .respawn gameobject guid #spawnGuid\r\nForces the gameobject with the given spawn GUID (database GUID) to respawn. Usable from console.'), +('respawn creature entry', 3, 'Syntax: .respawn creature entry #entry [#mapId [#instanceId]]\r\nIn-game: forces all creatures with the given #entry to respawn on the current map.\r\nConsole: specify #mapId (required) and optionally #instanceId. Pooled creatures are skipped in the queue phase to prevent duplicate spawns.'), +('respawn gameobject entry', 3, 'Syntax: .respawn gameobject entry #entry [#mapId [#instanceId]]\r\nIn-game: forces all gameobjects with the given #entry to respawn on the current map.\r\nConsole: specify #mapId (required) and optionally #instanceId. Pooled gameobjects are skipped in the queue phase to prevent duplicate spawns.'); diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index 1ea012c04d..d926ea7919 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -677,6 +677,10 @@ enum RBACPermissions RBAC_PERM_COMMAND_BF_QUEUE = 913, RBAC_PERM_COMMAND_PET_LIST = 914, RBAC_PERM_COMMAND_PET_DELETE = 915, + RBAC_PERM_COMMAND_RESPAWN_CREATURE_GUID = 916, + RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_GUID = 917, + RBAC_PERM_COMMAND_RESPAWN_CREATURE_ENTRY = 918, + RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY = 919, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index a0d1989018..c4ac6dbdb5 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1489,6 +1489,25 @@ enum AcoreStrings LANG_PET_DELETE_NOT_FOUND = 35438, LANG_PET_DELETE_SUCCESS = 35439, - LANG_CHARACTER_DELETED_LIST_LIMIT = 35440 + LANG_CHARACTER_DELETED_LIST_LIMIT = 35440, + + // Respawn creature/gameobject by spawn GUID commands + LANG_RESPAWN_GUID_CREATURE_NOT_FOUND = 35441, + LANG_RESPAWN_GUID_MAP_NOT_LOADED = 35442, + LANG_RESPAWN_GUID_CREATURE_ALIVE = 35443, + LANG_RESPAWN_GUID_CREATURE_QUEUED = 35444, + LANG_RESPAWN_GUID_GAMEOBJECT_ACTIVE = 35445, + LANG_RESPAWN_GUID_GAMEOBJECT_QUEUED = 35446, + // 35447 is reserved for LANG_LIST_RESPAWNS_NO_MAP (list respawns command, defined at the end of this enum) + LANG_RESPAWN_GUID_GAMEOBJECT_NOT_FOUND = 35448, + + // Respawn creature/gameobject by entry commands + LANG_RESPAWN_ENTRY_CREATURE_NOT_FOUND = 35449, + LANG_RESPAWN_ENTRY_GAMEOBJECT_NOT_FOUND = 35450, + LANG_RESPAWN_ENTRY_CREATURE_QUEUED = 35451, + LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED = 35452, + + // List respawns console support + LANG_LIST_RESPAWNS_NO_MAP = 35447 }; #endif diff --git a/src/server/scripts/Commands/cs_list.cpp b/src/server/scripts/Commands/cs_list.cpp index c098a7e30c..ecf82319c1 100644 --- a/src/server/scripts/Commands/cs_list.cpp +++ b/src/server/scripts/Commands/cs_list.cpp @@ -51,7 +51,7 @@ public: { "item", HandleListItemCommand, rbac::RBAC_PERM_COMMAND_LIST_ITEM, Console::Yes }, { "object", HandleListObjectCommand, rbac::RBAC_PERM_COMMAND_LIST_OBJECT, Console::Yes }, { "auras", listAurasCommandTable }, - { "respawns", HandleListRespawnsCommand, rbac::RBAC_PERM_COMMAND_LIST_CREATURE, Console::No }, + { "respawns", HandleListRespawnsCommand, rbac::RBAC_PERM_COMMAND_LIST_RESPAWNS, Console::Yes }, }; static ChatCommandTable commandTable = { @@ -521,13 +521,35 @@ public: return true; } - static bool HandleListRespawnsCommand(ChatHandler* handler) + static bool HandleListRespawnsCommand(ChatHandler* handler, Optional firstArg, Optional secondArg, Optional thirdArg) { - Player* player = handler->GetSession()->GetPlayer(); - if (!player) - return false; + Map* map = nullptr; + Optional entryFilter; + + if (handler->GetSession()) + { + // In-game: first arg = entryId (optional), use player's current map + map = handler->GetSession()->GetPlayer()->GetMap(); + entryFilter = firstArg; + } + else + { + // Console: first arg = mapId (required), second = instanceId, third = entryId + if (!firstArg) + { + handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP); + return false; + } + map = sMapMgr->FindMap(*firstArg, secondArg.value_or(0)); + entryFilter = thirdArg; + } + + if (!map) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, firstArg.value_or(0)); + return false; + } - Map* map = player->GetMap(); uint32 count = 0; time_t now = GameTime::GetGameTime().count(); @@ -535,7 +557,7 @@ public: for (auto const& pair : map->GetCreatureRespawnTimes()) { CreatureData const* data = sObjectMgr->GetCreatureData(pair.first); - if (!data) + if (!data || (entryFilter && data->id1 != *entryFilter)) continue; CreatureTemplate const* cTemplate = sObjectMgr->GetCreatureTemplate(data->id1); @@ -555,7 +577,7 @@ public: for (auto const& pair : map->GetGORespawnTimes()) { GameObjectData const* data = sObjectMgr->GetGameObjectData(pair.first); - if (!data) + if (!data || (entryFilter && data->id != *entryFilter)) continue; GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(data->id); diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index 961e9a5b99..22e4575063 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -24,6 +24,7 @@ #include "CommandScript.h" #include "Common.h" #include "GameGraveyard.h" +#include "GameObject.h" #include "GameTime.h" #include "GridNotifiers.h" #include "GridTerrainLoader.h" @@ -177,6 +178,10 @@ public: { "pinfo", HandlePInfoCommand, rbac::RBAC_PERM_COMMAND_PINFO, Console::Yes }, { "respawn", HandleRespawnCommand, rbac::RBAC_PERM_COMMAND_RESPAWN, Console::No }, { "respawn all", HandleRespawnAllCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_ALL, Console::No }, + { "respawn creature guid", HandleRespawnCreatureByGuidCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_CREATURE_GUID, Console::Yes }, + { "respawn gameobject guid", HandleRespawnGameObjectByGuidCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_GUID, Console::Yes }, + { "respawn creature entry", HandleRespawnCreatureByEntryCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_CREATURE_ENTRY, Console::Yes }, + { "respawn gameobject entry", HandleRespawnGameObjectByEntryCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY, Console::Yes }, { "mute", HandleMuteCommand, rbac::RBAC_PERM_COMMAND_MUTE, Console::Yes }, { "mutehistory", HandleMuteInfoCommand, rbac::RBAC_PERM_COMMAND_MUTEHISTORY, Console::Yes }, { "unmute", HandleUnmuteCommand, rbac::RBAC_PERM_COMMAND_UNMUTE, Console::Yes }, @@ -2448,6 +2453,265 @@ public: return true; } + static bool HandleRespawnCreatureByGuidCommand(ChatHandler* handler, ObjectGuid::LowType spawnId) + { + CreatureData const* creData = sObjectMgr->GetCreatureData(spawnId); + if (!creData) + { + handler->SendErrorMessage(LANG_RESPAWN_GUID_CREATURE_NOT_FOUND, spawnId); + return false; + } + + Map* map = nullptr; + if (handler->GetSession()) + { + Player* player = handler->GetSession()->GetPlayer(); + if (player->GetMapId() == creData->mapid) + map = player->GetMap(); + } + if (!map) + map = sMapMgr->FindMap(creData->mapid, 0); + + if (!map) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, creData->mapid); + return true; + } + + // First pass: check if any instance is alive + bool isAlive = false; + auto const creBounds = map->GetCreatureBySpawnIdStore().equal_range(spawnId); + for (auto itr = creBounds.first; itr != creBounds.second; ++itr) + { + if (itr->second->IsAlive()) + { + isAlive = true; + break; + } + } + + if (isAlive) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_CREATURE_ALIVE, spawnId, creData->id1); + return true; + } + + // Second pass: respawn any dead corpses in the world + for (auto itr = creBounds.first; itr != creBounds.second; ++itr) + { + if (itr->second->isDead()) + itr->second->Respawn(true); + } + // 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); + } + handler->PSendSysMessage(LANG_RESPAWN_GUID_CREATURE_QUEUED, spawnId, creData->id1); + return true; + } + + static bool HandleRespawnGameObjectByGuidCommand(ChatHandler* handler, ObjectGuid::LowType spawnId) + { + GameObjectData const* goData = sObjectMgr->GetGameObjectData(spawnId); + if (!goData) + { + handler->SendErrorMessage(LANG_RESPAWN_GUID_GAMEOBJECT_NOT_FOUND, spawnId); + return false; + } + + Map* map = nullptr; + if (handler->GetSession()) + { + Player* player = handler->GetSession()->GetPlayer(); + if (player->GetMapId() == goData->mapid) + map = player->GetMap(); + } + if (!map) + map = sMapMgr->FindMap(goData->mapid, 0); + + if (!map) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, goData->mapid); + return true; + } + + // First pass: check if any instance is already active + bool isActive = false; + auto const goBounds = map->GetGameObjectBySpawnIdStore().equal_range(spawnId); + for (auto itr = goBounds.first; itr != goBounds.second; ++itr) + { + if (itr->second->isSpawned()) + { + isActive = true; + break; + } + } + + if (isActive) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_GAMEOBJECT_ACTIVE, spawnId, goData->id); + return true; + } + + // Second pass: respawn inactive objects in the world + for (auto itr = goBounds.first; itr != goBounds.second; ++itr) + itr->second->Respawn(); + // Also trigger via respawn time queue for fully-removed spawns + if (map->GetGORespawnTime(spawnId) > 0) + { + time_t now = GameTime::GetGameTime().count(); + map->SaveGORespawnTime(spawnId, now); + } + handler->PSendSysMessage(LANG_RESPAWN_GUID_GAMEOBJECT_QUEUED, spawnId, goData->id); + return true; + } + + static bool HandleRespawnCreatureByEntryCommand(ChatHandler* handler, uint32 entry, Optional mapIdArg, Optional instanceIdArg) + { + if (!sObjectMgr->GetCreatureTemplate(entry)) + { + handler->SendErrorMessage(LANG_RESPAWN_ENTRY_CREATURE_NOT_FOUND, entry); + return false; + } + + Map* map = nullptr; + if (handler->GetSession()) + { + // In-game: always use the player's current map + map = handler->GetSession()->GetPlayer()->GetMap(); + } + else + { + // Console: mapId required, instanceId optional + if (!mapIdArg) + { + handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP); + return false; + } + map = sMapMgr->FindMap(*mapIdArg, instanceIdArg.value_or(0)); + } + + if (!map) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, mapIdArg.value_or(0)); + 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. + // Collect first to avoid iterator invalidation caused by Respawn(). + std::vector deadCreatures; + for (auto const& [spawnId, creature] : map->GetCreatureBySpawnIdStore()) + { + CreatureData const* data = sObjectMgr->GetCreatureData(spawnId); + if (!data || data->id1 != entry) + continue; + if (creature->isDead()) + deadCreatures.push_back(creature); + } + for (Creature* creature : deadCreatures) + { + creature->Respawn(true); + ++count; + } + + // Phase 2: set respawn time to now for fully-removed spawns, skipping pools + std::vector toRespawn; + for (auto const& [spawnId, respawnTime] : map->GetCreatureRespawnTimes()) + { + CreatureData const* data = sObjectMgr->GetCreatureData(spawnId); + if (!data || data->id1 != entry) + continue; + if (sPoolMgr->IsPartOfAPool(spawnId)) + continue; + toRespawn.push_back(spawnId); + } + for (ObjectGuid::LowType spawnId : toRespawn) + { + map->SaveCreatureRespawnTime(spawnId, now); + ++count; + } + + handler->PSendSysMessage(LANG_RESPAWN_ENTRY_CREATURE_QUEUED, count, entry); + return true; + } + + static bool HandleRespawnGameObjectByEntryCommand(ChatHandler* handler, uint32 entry, Optional mapIdArg, Optional instanceIdArg) + { + if (!sObjectMgr->GetGameObjectTemplate(entry)) + { + handler->SendErrorMessage(LANG_RESPAWN_ENTRY_GAMEOBJECT_NOT_FOUND, entry); + return false; + } + + Map* map = nullptr; + if (handler->GetSession()) + { + // In-game: always use the player's current map + map = handler->GetSession()->GetPlayer()->GetMap(); + } + else + { + // Console: mapId required, instanceId optional + if (!mapIdArg) + { + handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP); + return false; + } + map = sMapMgr->FindMap(*mapIdArg, instanceIdArg.value_or(0)); + } + + if (!map) + { + handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, mapIdArg.value_or(0)); + return false; + } + + time_t now = GameTime::GetGameTime().count(); + uint32 count = 0; + + // Phase 1: respawn inactive objects that are still tracked in the spawn-id store. + // Collect first to avoid iterator invalidation caused by Respawn(). + std::vector inactiveGOs; + for (auto const& [spawnId, go] : map->GetGameObjectBySpawnIdStore()) + { + GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId); + if (!data || data->id != entry) + continue; + if (!go->isSpawned()) + inactiveGOs.push_back(go); + } + for (GameObject* go : inactiveGOs) + { + go->Respawn(); + ++count; + } + + // Phase 2: set respawn time to now for fully-removed spawns, skipping pools + std::vector toRespawn; + for (auto const& [spawnId, respawnTime] : map->GetGORespawnTimes()) + { + GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId); + if (!data || data->id != entry) + continue; + if (sPoolMgr->IsPartOfAPool(spawnId)) + continue; + toRespawn.push_back(spawnId); + } + for (ObjectGuid::LowType spawnId : toRespawn) + { + map->SaveGORespawnTime(spawnId, now); + ++count; + } + + handler->PSendSysMessage(LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED, count, entry); + return true; + } + static bool HandleRespawnCommand(ChatHandler* handler) { Player* player = handler->GetSession()->GetPlayer();