diff --git a/data/sql/updates/pending_db_characters/rev_flush_respawn_times.sql b/data/sql/updates/pending_db_characters/rev_flush_respawn_times.sql new file mode 100644 index 000000000..1bd1f6836 --- /dev/null +++ b/data/sql/updates/pending_db_characters/rev_flush_respawn_times.sql @@ -0,0 +1,5 @@ +-- Flush all respawn times to ensure a clean slate for the spawn group system. +-- Compat-mode creatures/GOs will respawn naturally on next grid load; +-- non-compat spawns will be handled by ProcessRespawns(). +TRUNCATE TABLE `creature_respawn`; +TRUNCATE TABLE `gameobject_respawn`; diff --git a/data/sql/updates/pending_db_world/rev_spawn_group_tables.sql b/data/sql/updates/pending_db_world/rev_spawn_group_tables.sql new file mode 100644 index 000000000..ee8eb847d --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_spawn_group_tables.sql @@ -0,0 +1,68 @@ +-- +DROP TABLE IF EXISTS `spawn_group_template`; +CREATE TABLE `spawn_group_template` ( + `groupId` int(10) unsigned NOT NULL, + `groupName` varchar(100) NOT NULL, + `groupFlags` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`groupId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `spawn_group`; +CREATE TABLE `spawn_group` ( + `groupId` int(10) unsigned NOT NULL, + `spawnType` tinyint(3) unsigned NOT NULL, + `spawnId` int(10) unsigned NOT NULL, + PRIMARY KEY (`groupId`,`spawnType`,`spawnId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Insert default spawn groups +DELETE FROM `spawn_group_template` WHERE `groupId` IN (0, 1); +INSERT INTO `spawn_group_template` (`groupId`, `groupName`, `groupFlags`) VALUES +(0, 'Default Group', 0x01), -- SYSTEM (dynamic respawn by default) +(1, 'Legacy Group', 0x03); -- SYSTEM | COMPATIBILITY_MODE + +-- Register spawn group commands +DELETE FROM `command` WHERE `name` IN ('list respawns', 'npc spawngroup', 'npc despawngroup', 'gobject spawngroup', 'gobject despawngroup', 'reload spawn_group'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('list respawns', 2, 'Syntax: .list respawns\r\nShows all pending creature and gameobject respawns on the current map.'), +('npc spawngroup', 3, 'Syntax: .npc spawngroup #groupId\r\nSpawns all creatures in the given spawn group.'), +('npc despawngroup', 3, 'Syntax: .npc despawngroup #groupId\r\nDespawns all creatures in the given spawn group.'), +('gobject spawngroup', 3, 'Syntax: .gobject spawngroup #groupId\r\nSpawns all gameobjects in the given spawn group.'), +('gobject despawngroup', 3, 'Syntax: .gobject despawngroup #groupId\r\nDespawns all gameobjects in the given spawn group.'), +('reload spawn_group', 3, 'Syntax: .reload spawn_group\r\nReloads the spawn_group_template and spawn_group tables.'); + +-- Spawn group localized strings +DELETE FROM `acore_string` WHERE `entry` BETWEEN 35411 AND 35424; +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35411, 'Cannot manually spawn system group {} ({}).', '시스템 그룹 {} ({})을(를) 수동으로 생성할 수 없습니다.', 'Impossible de faire apparaître manuellement le groupe système {} ({}).', 'Systemgruppe {} ({}) kann nicht manuell gespawnt werden.', '无法手动生成系统组 {} ({})。', '無法手動生成系統組 {} ({})。', 'No se puede generar manualmente el grupo del sistema {} ({}).', 'No se puede generar manualmente el grupo del sistema {} ({}).', 'Невозможно вручную создать системную группу {} ({}).'), +(35412, 'Spawn group {} ({}) spawned successfully.', '스폰 그룹 {} ({})이(가) 성공적으로 생성되었습니다.', 'Le groupe d''apparition {} ({}) a été créé avec succès.', 'Spawngruppe {} ({}) erfolgreich erstellt.', '刷新组 {} ({}) 已成功生成。', '重生組 {} ({}) 已成功生成。', 'Grupo de aparición {} ({}) generado correctamente.', 'Grupo de aparición {} ({}) generado correctamente.', 'Группа спавна {} ({}) успешно создана.'), +(35413, 'Failed to spawn group {} ({}).', '스폰 그룹 {} ({}) 생성에 실패했습니다.', 'Échec de l''apparition du groupe {} ({}).', 'Spawngruppe {} ({}) konnte nicht erstellt werden.', '生成组 {} ({}) 失败。', '生成組 {} ({}) 失敗。', 'Error al generar el grupo {} ({}).', 'Error al generar el grupo {} ({}).', 'Не удалось создать группу спавна {} ({}).'), +(35414, 'Cannot manually despawn system group {} ({}).', '시스템 그룹 {} ({})을(를) 수동으로 제거할 수 없습니다.', 'Impossible de retirer manuellement le groupe système {} ({}).', 'Systemgruppe {} ({}) kann nicht manuell entfernt werden.', '无法手动移除系统组 {} ({})。', '無法手動移除系統組 {} ({})。', 'No se puede eliminar manualmente el grupo del sistema {} ({}).', 'No se puede eliminar manualmente el grupo del sistema {} ({}).', 'Невозможно вручную удалить системную группу {} ({}).'), +(35415, 'Spawn group {} ({}) despawned successfully.', '스폰 그룹 {} ({})이(가) 성공적으로 제거되었습니다.', 'Le groupe d''apparition {} ({}) a été retiré avec succès.', 'Spawngruppe {} ({}) erfolgreich entfernt.', '刷新组 {} ({}) 已成功移除。', '重生組 {} ({}) 已成功移除。', 'Grupo de aparición {} ({}) eliminado correctamente.', 'Grupo de aparición {} ({}) eliminado correctamente.', 'Группа спавна {} ({}) успешно удалена.'), +(35416, 'Failed to despawn group {} ({}).', '스폰 그룹 {} ({}) 제거에 실패했습니다.', 'Échec du retrait du groupe {} ({}).', 'Spawngruppe {} ({}) konnte nicht entfernt werden.', '移除组 {} ({}) 失败。', '移除組 {} ({}) 失敗。', 'Error al eliminar el grupo {} ({}).', 'Error al eliminar el grupo {} ({}).', 'Не удалось удалить группу спавна {} ({}).'), +(35419, 'Pending creature respawns on map {} (instance {}):', '맵 {} (인스턴스 {})의 대기 중인 생물 리스폰:', 'Réapparitions de créatures en attente sur la carte {} (instance {}) :', 'Ausstehende Kreatur-Respawns auf Karte {} (Instanz {}):', '地图 {} (副本 {}) 上待处理的生物重生:', '地圖 {} (副本 {}) 上待處理的生物重生:', 'Reapariciones de criaturas pendientes en mapa {} (instancia {}):', 'Reapariciones de criaturas pendientes en mapa {} (instancia {}):', 'Ожидающие респауны существ на карте {} (инстанс {}):'), +(35420, ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}초', ' DB GUID : {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}秒', ' DB GUID: {} - {} ({}) - {}秒', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}с'), +(35421, 'Pending gameobject respawns:', '대기 중인 게임오브젝트 리스폰:', 'Réapparitions de game objects en attente :', 'Ausstehende Spielobjekt-Respawns:', '待处理的游戏对象重生:', '待處理的遊戲物件重生:', 'Reapariciones de objetos pendientes:', 'Reapariciones de objetos pendientes:', 'Ожидающие респауны игровых объектов:'), +(35422, ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}초', ' DB GUID : {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}秒', ' DB GUID: {} - {} ({}) - {}秒', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}s', ' DB GUID: {} - {} ({}) - {}с'), +(35423, ' ... and more (limited to 50)', ' ... 그 외 다수 (50개로 제한)', ' ... et plus (limité à 50)', ' ... und mehr (auf 50 begrenzt)', ' ... 以及更多(限制为50)', ' ... 以及更多(限制為50)', ' ... y más (limitado a 50)', ' ... y más (limitado a 50)', ' ... и ещё (ограничено 50)'), +(35424, 'Spawn group {} not found.', '스폰 그룹 {}을(를) 찾을 수 없습니다.', 'Groupe d''apparition {} introuvable.', 'Spawngruppe {} nicht gefunden.', '刷新组 {} 未找到。', '重生組 {} 未找到。', 'Grupo de aparición {} no encontrado.', 'Grupo de aparición {} no encontrado.', 'Группа спавна {} не найдена.'); + +-- Register pool debug commands +DELETE FROM `command` WHERE `name` IN ('pool info', 'pool lookup'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('pool info', 2, 'Syntax: .pool info #poolId\r\nShows pool details: description, max active, all creature/gameobject/sub-pool members with active/inactive status.'), +('pool lookup', 2, 'Syntax: .pool lookup\r\nTarget a creature or stand near a gameobject to find which pool it belongs to and its current spawn status.'); + +-- Pool debug command localized strings +DELETE FROM `acore_string` WHERE `entry` BETWEEN 35425 AND 35434; +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35425, 'Pool {} does not exist.', '풀 {}이(가) 존재하지 않습니다.', 'Le pool {} n''existe pas.', 'Pool {} existiert nicht.', '池 {} 不存在。', '池 {} 不存在。', 'El pool {} no existe.', 'El pool {} no existe.', 'Пул {} не существует.'), +(35426, '=== Pool {} | {} | max active: {} | spawned: {} ===', '=== 풀 {} | {} | 최대 활성: {} | 생성됨: {} ===', '=== Pool {} | {} | max actifs : {} | apparus : {} ===', '=== Pool {} | {} | max. aktiv: {} | gespawnt: {} ===', '=== 池 {} | {} | 最大活跃: {} | 已生成: {} ===', '=== 池 {} | {} | 最大活躍: {} | 已生成: {} ===', '=== Pool {} | {} | máx activos: {} | generados: {} ===', '=== Pool {} | {} | máx activos: {} | generados: {} ===', '=== Пул {} | {} | макс. активных: {} | создано: {} ==='), +(35427, ' {} ({} total):', ' {} (총 {}):', ' {} ({} au total) :', ' {} ({} insgesamt):', ' {}(共 {}):', ' {}(共 {}):', ' {} ({} en total):', ' {} ({} en total):', ' {} (всего {}):'), +(35428, ' [{}] GUID {} | {} (entry {}) | map {} ({}, {}, {})', ' [{}] GUID {} | {} (항목 {}) | 맵 {} ({}, {}, {})', ' [{}] GUID {} | {} (entrée {}) | carte {} ({}, {}, {})', ' [{}] GUID {} | {} (Eintrag {}) | Karte {} ({}, {}, {})', ' [{}] GUID {} | {} (条目 {}) | 地图 {} ({}, {}, {})', ' [{}] GUID {} | {} (條目 {}) | 地圖 {} ({}, {}, {})', ' [{}] GUID {} | {} (entrada {}) | mapa {} ({}, {}, {})', ' [{}] GUID {} | {} (entrada {}) | mapa {} ({}, {}, {})', ' [{}] GUID {} | {} (запись {}) | карта {} ({}, {}, {})'), +(35429, ' Sub-pools ({} total):', ' 하위 풀 (총 {}):', ' Sous-pools ({} au total) :', ' Unter-Pools ({} insgesamt):', ' 子池(共 {}):', ' 子池(共 {}):', ' Sub-pools ({} en total):', ' Sub-pools ({} en total):', ' Под-пулы (всего {}):'), +(35430, ' [{}] Pool {} | {}', ' [{}] 풀 {} | {}', ' [{}] Pool {} | {}', ' [{}] Pool {} | {}', ' [{}] 池 {} | {}', ' [{}] 池 {} | {}', ' [{}] Pool {} | {}', ' [{}] Pool {} | {}', ' [{}] Пул {} | {}'), +(35431, '{} (GUID {}) is in pool {}. Status: {}', '{} (GUID {})이(가) 풀 {}에 있습니다. 상태: {}', '{} (GUID {}) est dans le pool {}. Statut : {}', '{} (GUID {}) ist in Pool {}. Status: {}', '{} (GUID {}) 在池 {} 中。状态:{}', '{} (GUID {}) 在池 {} 中。狀態:{}', '{} (GUID {}) está en el pool {}. Estado: {}', '{} (GUID {}) está en el pool {}. Estado: {}', '{} (GUID {}) в пуле {}. Статус: {}'), +(35432, '{} (GUID {}) is not in any pool.', '{} (GUID {})은(는) 어떤 풀에도 속해 있지 않습니다.', '{} (GUID {}) n''est dans aucun pool.', '{} (GUID {}) ist in keinem Pool.', '{} (GUID {}) 不在任何池中。', '{} (GUID {}) 不在任何池中。', '{} (GUID {}) no está en ningún pool.', '{} (GUID {}) no está en ningún pool.', '{} (GUID {}) не в пуле.'), +(35433, ' Use ''.pool info {}'' for full details.', ' 전체 세부정보는 ''.pool info {}''를 사용하세요.', ' Utilisez ''.pool info {}'' pour les détails complets.', ' Verwenden Sie ''.pool info {}'' für vollständige Details.', ' 使用".pool info {}"查看完整详情。', ' 使用「.pool info {}」查看完整詳情。', ' Use ''.pool info {}'' para ver los detalles completos.', ' Use ''.pool info {}'' para ver los detalles completos.', ' Используйте ''.pool info {}'' для полной информации.'), +(35434, 'No creature selected and no nearby gameobject found. Target a creature or stand near a gameobject.', '선택된 생물이 없고 근처 게임오브젝트도 없습니다. 생물을 대상으로 지정하거나 게임오브젝트 근처에 서세요.', 'Aucune créature sélectionnée et aucun objet de jeu à proximité. Ciblez une créature ou approchez-vous d''un objet de jeu.', 'Keine Kreatur ausgewählt und kein Spielobjekt in der Nähe gefunden. Wählen Sie eine Kreatur oder stehen Sie neben einem Spielobjekt.', '未选择生物且附近没有游戏对象。请选择一个生物或站在游戏对象旁边。', '未選擇生物且附近沒有遊戲物件。請選擇一個生物或站在遊戲物件旁邊。', 'No hay criatura seleccionada ni objeto cercano. Seleccione una criatura o acérquese a un objeto.', 'No hay criatura seleccionada ni objeto cercano. Seleccione una criatura o acérquese a un objeto.', 'Существо не выбрано и рядом нет игровых объектов. Выберите существо или встаньте рядом с объектом.'); diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 361b32e8e..675f46974 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -4754,6 +4754,27 @@ Respawn.DynamicRateGameObject = 1 Respawn.DynamicMinimumGameObject = 10 +# +# Respawn.DynamicEscortNPC +# Description: Enable dynamic respawn behavior for escort quest NPCs. +# When enabled, escort NPCs in spawn groups flagged as ESCORTQUESTNPC +# will use special respawn handling. +# Default: 0 - (Disabled) +# + +Respawn.DynamicEscortNPC = 0 + +# +# Respawn.ForceCompatibilityMode +# Description: Force all spawns to use legacy (compatibility mode) respawn behavior, +# regardless of their spawn group flags. When enabled, creatures and +# gameobjects respawn in-place as they always have in AzerothCore. +# Set to 1 to force legacy behavior for all spawns. +# Default: 0 - (Disabled, spawn groups control respawn mode) +# + +Respawn.ForceCompatibilityMode = 0 + # ################################################################################################### diff --git a/src/server/database/Database/Implementation/WorldDatabase.cpp b/src/server/database/Database/Implementation/WorldDatabase.cpp index 128c0c7c0..7d580068b 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.cpp +++ b/src/server/database/Database/Implementation/WorldDatabase.cpp @@ -111,6 +111,7 @@ void WorldDatabaseConnection::DoPrepareStatements() // 0: uint8 PrepareStatement(WORLD_SEL_REQ_XP, "SELECT Experience FROM player_xp_for_level WHERE Level = ?", CONNECTION_SYNCH); PrepareStatement(WORLD_UPD_VERSION, "UPDATE version SET core_version = ?, core_revision = ?", CONNECTION_ASYNC); + PrepareStatement(WORLD_DEL_SPAWNGROUP_MEMBER, "DELETE FROM spawn_group WHERE spawnType = ? AND spawnId = ?", CONNECTION_ASYNC); } WorldDatabaseConnection::WorldDatabaseConnection(MySQLConnectionInfo& connInfo) : MySQLConnection(connInfo) diff --git a/src/server/database/Database/Implementation/WorldDatabase.h b/src/server/database/Database/Implementation/WorldDatabase.h index d80c854ea..3c0ad9c15 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.h +++ b/src/server/database/Database/Implementation/WorldDatabase.h @@ -116,6 +116,7 @@ enum WorldDatabaseStatements : uint32 WORLD_SEL_REQ_XP, WORLD_INS_GAMEOBJECT_ADDON, WORLD_UPD_VERSION, + WORLD_DEL_SPAWNGROUP_MEMBER, MAX_WORLDDATABASE_STATEMENTS }; diff --git a/src/server/game/AI/CreatureAI.h b/src/server/game/AI/CreatureAI.h index 06d250a38..a330878ab 100644 --- a/src/server/game/AI/CreatureAI.h +++ b/src/server/game/AI/CreatureAI.h @@ -118,6 +118,9 @@ public: // Called in Creature::Update when deathstate = DEAD. Inherited classes may maniuplate the ability to respawn based on scripted events. virtual bool CanRespawn() { return true; } + // Whether this creature is an escort NPC (override in escort AI) + virtual bool IsEscortNPC(bool /*onlyIfActive*/ = true) const { return false; } + // Called for reaction at stopping attack at no attackers or targets virtual void EnterEvadeMode(EvadeReason why = EVADE_REASON_OTHER); diff --git a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h index 83039da38..d56e57804 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h +++ b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h @@ -64,6 +64,8 @@ public: void JustRespawned() override; + bool IsEscortNPC(bool /*onlyIfActive*/ = true) const override { return true; } + void ReturnToLastPoint(); void EnterEvadeMode(EvadeReason /*why*/ = EVADE_REASON_OTHER) override; diff --git a/src/server/game/AI/SmartScripts/SmartScript.cpp b/src/server/game/AI/SmartScripts/SmartScript.cpp index 27b390bd0..fcd03bd24 100644 --- a/src/server/game/AI/SmartScripts/SmartScript.cpp +++ b/src/server/game/AI/SmartScripts/SmartScript.cpp @@ -2968,6 +2968,27 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u } break; } + case SMART_ACTION_SPAWN_SPAWNGROUP: + { + WorldObject* obj = GetBaseObject(); + if (!obj) + break; + + obj->GetMap()->SpawnGroupSpawn(e.action.groupSpawn.groupId, + e.action.groupSpawn.ignoreRespawn != 0, + e.action.groupSpawn.force != 0); + break; + } + case SMART_ACTION_DESPAWN_SPAWNGROUP: + { + WorldObject* obj = GetBaseObject(); + if (!obj) + break; + + obj->GetMap()->SpawnGroupDespawn(e.action.groupSpawn.groupId, + e.action.groupSpawn.ignoreRespawn != 0); // reuse ignoreRespawn as deleteRespawnTimes + break; + } case SMART_ACTION_SET_GUID: { for (WorldObject* target : targets) diff --git a/src/server/game/AI/SmartScripts/SmartScriptMgr.cpp b/src/server/game/AI/SmartScripts/SmartScriptMgr.cpp index 847856b35..4b90be524 100644 --- a/src/server/game/AI/SmartScripts/SmartScriptMgr.cpp +++ b/src/server/game/AI/SmartScripts/SmartScriptMgr.cpp @@ -846,8 +846,8 @@ bool SmartAIMgr::CheckUnusedActionParams(SmartScriptHolder const& e) case SMART_ACTION_PLAY_ANIMKIT: return sizeof(SmartAction::raw); case SMART_ACTION_SCENE_PLAY: return sizeof(SmartAction::raw); case SMART_ACTION_SCENE_CANCEL: return sizeof(SmartAction::raw); - // case SMART_ACTION_SPAWN_SPAWNGROUP: return sizeof(SmartAction::groupSpawn); - // case SMART_ACTION_DESPAWN_SPAWNGROUP: return sizeof(SmartAction::groupSpawn); + case SMART_ACTION_SPAWN_SPAWNGROUP: return sizeof(SmartAction::groupSpawn); + case SMART_ACTION_DESPAWN_SPAWNGROUP: return sizeof(SmartAction::groupSpawn); // case SMART_ACTION_RESPAWN_BY_SPAWNID: return sizeof(SmartAction::respawnData); case SMART_ACTION_PLAY_CINEMATIC: return sizeof(SmartAction::cinematic); case SMART_ACTION_SET_MOVEMENT_SPEED: return sizeof(SmartAction::movementSpeed); @@ -1013,12 +1013,21 @@ bool SmartAIMgr::IsEventValid(SmartScriptHolder& e) case SMART_ACTION_SET_CAN_FLY: case SMART_ACTION_REMOVE_AURAS_BY_TYPE: case SMART_ACTION_REMOVE_MOVEMENT: - case SMART_ACTION_SPAWN_SPAWNGROUP: - case SMART_ACTION_DESPAWN_SPAWNGROUP: case SMART_ACTION_RESPAWN_BY_SPAWNID: LOG_ERROR("sql.sql", "SmartAIMgr: EntryOrGuid {} using event({}) has an action type that is not yet supported on AzerothCore ({}), skipped.", e.entryOrGuid, e.event_id, e.GetActionType()); return false; + case SMART_ACTION_SPAWN_SPAWNGROUP: + case SMART_ACTION_DESPAWN_SPAWNGROUP: + { + if (!sObjectMgr->GetSpawnGroupData(e.action.groupSpawn.groupId)) + { + LOG_ERROR("sql.sql", "SmartAIMgr: EntryOrGuid {} using event({}) has action type {} with invalid spawn group id {}.", + e.entryOrGuid, e.event_id, e.GetActionType(), e.action.groupSpawn.groupId); + return false; + } + break; + } default: break; } diff --git a/src/server/game/AI/SmartScripts/SmartScriptMgr.h b/src/server/game/AI/SmartScripts/SmartScriptMgr.h index 8163866e2..c265f582e 100644 --- a/src/server/game/AI/SmartScripts/SmartScriptMgr.h +++ b/src/server/game/AI/SmartScripts/SmartScriptMgr.h @@ -670,8 +670,8 @@ enum SMART_ACTION SMART_ACTION_PLAY_ANIMKIT = 128, // don't use on 3.3.5a SMART_ACTION_SCENE_PLAY = 129, // don't use on 3.3.5a SMART_ACTION_SCENE_CANCEL = 130, // don't use on 3.3.5a - SMART_ACTION_SPAWN_SPAWNGROUP = 131, /// @todo: NOT SUPPORTED YET - SMART_ACTION_DESPAWN_SPAWNGROUP = 132, /// @todo: NOT SUPPORTED YET + SMART_ACTION_SPAWN_SPAWNGROUP = 131, // groupId, ignoreRespawn, force + SMART_ACTION_DESPAWN_SPAWNGROUP = 132, // groupId, deleteRespawnTimes SMART_ACTION_RESPAWN_BY_SPAWNID = 133, /// @todo: NOT SUPPORTED YET SMART_ACTION_INVOKER_CAST = 134, // spellID, castFlags, triggerFlags, targetsLimit SMART_ACTION_PLAY_CINEMATIC = 135, // entry @@ -1523,6 +1523,13 @@ struct SmartAction { uint32 group; } gameobjectGroup; + + struct + { + uint32 groupId; + uint32 ignoreRespawn; + uint32 force; + } groupSpawn; }; }; diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 14cbd71cf..04cda14ad 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -420,34 +420,55 @@ void Creature::RemoveCorpse(bool setSpawnTime, bool skipVisibility) if (getDeathState() != DeathState::Corpse) return; - m_corpseRemoveTime = GameTime::GetGameTime().count(); - setDeathState(DeathState::Dead); - RemoveAllAuras(); - if (!skipVisibility) // pussywizard - DestroyForVisiblePlayers(); // pussywizard: previous UpdateObjectVisibility() - loot.clear(); - uint32 respawnDelay = m_respawnDelay; - if (IsAIEnabled) - AI()->CorpseRemoved(respawnDelay); - - // Should get removed later, just keep "compatibility" with scripts - if (setSpawnTime) + if (_respawnCompatibilityMode) { - m_respawnTime = GameTime::GetGameTime().count() + respawnDelay; - //SaveRespawnTime(); + m_corpseRemoveTime = GameTime::GetGameTime().count(); + setDeathState(DeathState::Dead); + RemoveAllAuras(); + if (!skipVisibility) // pussywizard + DestroyForVisiblePlayers(); // pussywizard: previous UpdateObjectVisibility() + loot.clear(); + uint32 respawnDelay = m_respawnDelay; + if (IsAIEnabled) + AI()->CorpseRemoved(respawnDelay); + + // Should get removed later, just keep "compatibility" with scripts + if (setSpawnTime) + { + m_respawnTime = GameTime::GetGameTime().count() + respawnDelay; + //SaveRespawnTime(); + } + + float x, y, z, o; + GetRespawnPosition(x, y, z, &o); + SetHomePosition(x, y, z, o); + SetPosition(x, y, z, o); + + // xinef: relocate notifier + m_last_notify_position.Relocate(-5000.0f, -5000.0f, -5000.0f, 0.0f); + + // pussywizard: if corpse was removed during falling then the falling will continue after respawn, so stop falling is such case + if (IsFalling()) + StopMoving(); } + else + { + // Dynamic spawn mode: save respawn time and remove the object entirely. + // A fresh creature will be spawned by ProcessRespawns() when the timer expires. + loot.clear(); + uint32 respawnDelay = m_respawnDelay; + if (IsAIEnabled) + AI()->CorpseRemoved(respawnDelay); - float x, y, z, o; - GetRespawnPosition(x, y, z, &o); - SetHomePosition(x, y, z, o); - SetPosition(x, y, z, o); + // Always save respawn time in non-compat mode since the creature is being + // destroyed — ProcessRespawns() needs the entry to know when to recreate it. + // m_respawnTime was already set in setDeathState(JustDied). + if (setSpawnTime) + m_respawnTime = std::max(GameTime::GetGameTime().count() + respawnDelay, m_respawnTime); + SaveRespawnTime(); - // xinef: relocate notifier - m_last_notify_position.Relocate(-5000.0f, -5000.0f, -5000.0f, 0.0f); - - // pussywizard: if corpse was removed during falling then the falling will continue after respawn, so stop falling is such case - if (IsFalling()) - StopMoving(); + AddObjectToRemoveList(); + } } /** @@ -1696,6 +1717,11 @@ bool Creature::LoadCreatureFromDB(ObjectGuid::LowType spawnId, Map* map, bool ad m_creatureData = data; m_spawnId = spawnId; + // Set respawn compatibility mode based on spawn group flags + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(data->spawnGroupId); + _respawnCompatibilityMode = sWorld->getBoolConfig(CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE) + || !groupData || (groupData->flags & SPAWNGROUP_FLAG_COMPATIBILITY_MODE); + // Add to world uint32 entry = GetRandomId(data->id1, data->id2, data->id3); @@ -2020,74 +2046,90 @@ void Creature::Respawn(bool force) if (!linkedRespawntime || (cInfo && cInfo->HasFlagsExtra(CREATURE_FLAG_EXTRA_HARD_RESET)) || force) // Should respawn { - RemoveCorpse(false, false); - - if (getDeathState() == DeathState::Dead) + if (_respawnCompatibilityMode) { + RemoveCorpse(false, false); + + if (getDeathState() == DeathState::Dead) + { + if (m_spawnId) + { + GetMap()->RemoveCreatureRespawnTime(m_spawnId); + CreatureData const* data = sObjectMgr->GetCreatureData(m_spawnId); + // Respawn check if spawn has 2 entries + if (data->id2) + { + uint32 entry = GetRandomId(data->id1, data->id2, data->id3); + UpdateEntry(entry, data, true); // Select Random Entry + m_defaultMovementType = MovementGeneratorType(data->movementType); // Reload Movement Type + LoadEquipment(data->equipmentId); // Reload Equipment + AIM_Initialize(); // Reload AI + } + else + { + if (m_originalEntry != GetEntry()) + UpdateEntry(m_originalEntry); + } + } + + LOG_DEBUG("entities.unit", "Respawning creature {} (SpawnId: {}, {})", GetName(), GetSpawnId(), GetGUID().ToString()); + m_respawnTime = 0; + ResetPickPocketLootTime(); + loot.clear(); + SelectLevel(); + + m_respawnedTime = GameTime::GetGameTime().count(); + setDeathState(DeathState::JustRespawned); + + // MDic - Acidmanifesto: Do not override transform auras + if (GetAuraEffectsByType(SPELL_AURA_TRANSFORM).empty()) + { + CreatureModel display(GetNativeDisplayId(), GetNativeObjectScale(), 1.0f); + CreatureModelInfo const* minfo = sObjectMgr->GetCreatureModelRandomGender(&display, GetCreatureTemplate()); + if (minfo) // Cancel load if no model defined + { + SetDisplayId(display.CreatureDisplayID, display.DisplayScale); + SetNativeDisplayId(display.CreatureDisplayID); + } + } + + GetMotionMaster()->InitDefault(); + + //Call AI respawn virtual function + if (IsAIEnabled) + { + //reset the AI to be sure no dirty or uninitialized values will be used till next tick + AI()->Reset(); + TriggerJustRespawned = true; //delay event to next tick so all creatures are created on the map before processing + } + + uint32 poolid = m_spawnId ? sPoolMgr->IsPartOfAPool(m_spawnId) : 0; + if (poolid) + sPoolMgr->UpdatePool(poolid, m_spawnId); + + //Re-initialize reactstate that could be altered by movementgenerators + InitializeReactState(); + + } + m_respawnedTime = GameTime::GetGameTime().count(); + // xinef: relocate notifier, fixes npc appearing in corpse position after forced respawn (instead of spawn) + m_last_notify_position.Relocate(-5000.0f, -5000.0f, -5000.0f, 0.0f); + UpdateObjectVisibility(false); + } + else + { + // Non-compat mode: destroy and let ProcessRespawns() recreate + if (IsAlive()) + return; + if (m_spawnId) { - GetMap()->RemoveCreatureRespawnTime(m_spawnId); - CreatureData const* data = sObjectMgr->GetCreatureData(m_spawnId); - // Respawn check if spawn has 2 entries - if (data->id2) - { - uint32 entry = GetRandomId(data->id1, data->id2, data->id3); - UpdateEntry(entry, data, true); // Select Random Entry - m_defaultMovementType = MovementGeneratorType(data->movementType); // Reload Movement Type - LoadEquipment(data->equipmentId); // Reload Equipment - AIM_Initialize(); // Reload AI - } - else - { - if (m_originalEntry != GetEntry()) - UpdateEntry(m_originalEntry); - } + // Set respawn time to now so ProcessRespawns() picks it up + time_t now = GameTime::GetGameTime().count(); + GetMap()->SaveCreatureRespawnTime(m_spawnId, now); } - - LOG_DEBUG("entities.unit", "Respawning creature {} (SpawnId: {}, {})", GetName(), GetSpawnId(), GetGUID().ToString()); - m_respawnTime = 0; - ResetPickPocketLootTime(); - loot.clear(); - SelectLevel(); - - m_respawnedTime = GameTime::GetGameTime().count(); - setDeathState(DeathState::JustRespawned); - - // MDic - Acidmanifesto: Do not override transform auras - if (GetAuraEffectsByType(SPELL_AURA_TRANSFORM).empty()) - { - CreatureModel display(GetNativeDisplayId(), GetNativeObjectScale(), 1.0f); - CreatureModelInfo const* minfo = sObjectMgr->GetCreatureModelRandomGender(&display, GetCreatureTemplate()); - if (minfo) // Cancel load if no model defined - { - SetDisplayId(display.CreatureDisplayID, display.DisplayScale); - SetNativeDisplayId(display.CreatureDisplayID); - } - } - - GetMotionMaster()->InitDefault(); - - //Call AI respawn virtual function - if (IsAIEnabled) - { - //reset the AI to be sure no dirty or uninitialized values will be used till next tick - AI()->Reset(); - TriggerJustRespawned = true; //delay event to next tick so all creatures are created on the map before processing - } - - uint32 poolid = m_spawnId ? sPoolMgr->IsPartOfAPool(m_spawnId) : 0; - if (poolid) - sPoolMgr->UpdatePool(poolid, m_spawnId); - - //Re-initialize reactstate that could be altered by movementgenerators - InitializeReactState(); - + AddObjectToRemoveList(); } - m_respawnedTime = GameTime::GetGameTime().count(); - // xinef: relocate notifier, fixes npc appearing in corpse position after forced respawn (instead of spawn) - m_last_notify_position.Relocate(-5000.0f, -5000.0f, -5000.0f, 0.0f); - UpdateObjectVisibility(false); - } else // the master is dead { @@ -2114,13 +2156,27 @@ void Creature::ForcedDespawn(Milliseconds timeMSToDespawn, Seconds forceRespawnT return; } + // Override respawn delay BEFORE setDeathState, because setDeathState(JustDied) + // computes m_respawnTime = now + m_respawnDelay + m_corpseDelay and immediately + // saves it to DB for bosses/elites. We must have the correct delay in place + // before that happens. + if (forceRespawnTimer > 0s) + m_respawnDelay = forceRespawnTimer.count(); + if (IsAlive()) setDeathState(DeathState::JustDied, true); // Xinef: Set new respawn time, ignore corpse decay time... + // After setDeathState, m_respawnTime includes m_corpseDelay which we don't + // want for a forced respawn. Override it so RemoveCorpse's max() picks ours. + if (forceRespawnTimer > 0s) + m_respawnTime = GameTime::GetGameTime().count() + forceRespawnTimer.count(); + RemoveCorpse(true); - if (forceRespawnTimer > 0s) + // In compat mode the creature stays in the world as a dead body and needs + // an event-based kick to call Respawn() after the timer expires. + if (forceRespawnTimer > 0s && _respawnCompatibilityMode) if (GetMap()) GetMap()->ScheduleCreatureRespawn(GetGUID(), forceRespawnTimer); } diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index f95946ab9..7e86608af 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -221,6 +221,7 @@ public: bool LoadFromDB(ObjectGuid::LowType guid, Map* map, bool allowDuplicate = false) { return LoadCreatureFromDB(guid, map, false, allowDuplicate); } bool LoadCreatureFromDB(ObjectGuid::LowType guid, Map* map, bool addToMap = true, bool allowDuplicate = false); + [[nodiscard]] bool IsRespawnCompatibilityMode() const { return _respawnCompatibilityMode; } void SaveToDB(); virtual void SaveToDB(uint32 mapid, uint8 spawnMask, uint32 phaseMask); // overriden in Pet @@ -465,6 +466,8 @@ protected: ObjectGuid m_lootRecipient; ObjectGuid::LowType m_lootRecipientGroup; + bool _respawnCompatibilityMode{true}; + /// Timers time_t m_corpseRemoveTime; // (secs) timer for death or corpse disappearance time_t m_respawnTime; // (secs) time of next respawn diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index 7885b3bfc..bb5dabd98 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -1121,6 +1121,11 @@ bool GameObject::LoadGameObjectFromDB(ObjectGuid::LowType spawnId, Map* map, boo m_goData = data; m_spawnId = spawnId; + // Set respawn compatibility mode based on spawn group flags + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(data->spawnGroupId); + _respawnCompatibilityMode = sWorld->getBoolConfig(CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE) + || !groupData || (groupData->flags & SPAWNGROUP_FLAG_COMPATIBILITY_MODE); + if (!Create(map->GenerateLowGuid(), entry, map, phaseMask, x, y, z, ang, data->rotation, animprogress, go_state, artKit)) return false; diff --git a/src/server/game/Entities/GameObject/GameObject.h b/src/server/game/Entities/GameObject/GameObject.h index 8c4a943e0..3cae54de9 100644 --- a/src/server/game/Entities/GameObject/GameObject.h +++ b/src/server/game/Entities/GameObject/GameObject.h @@ -158,6 +158,7 @@ public: void SaveToDB(uint32 mapid, uint8 spawnMask, uint32 phaseMask, bool saveAddon = false); virtual bool LoadFromDB(ObjectGuid::LowType guid, Map* map) { return LoadGameObjectFromDB(guid, map, false); } virtual bool LoadGameObjectFromDB(ObjectGuid::LowType guid, Map* map, bool addToMap = true); + [[nodiscard]] bool IsRespawnCompatibilityMode() const { return _respawnCompatibilityMode; } void DeleteFromDB(); void SetOwnerGUID(ObjectGuid owner) @@ -369,6 +370,7 @@ protected: bool AIM_Initialize(); GameObjectModel* CreateModel(); void UpdateModel(); // updates model in case displayId were changed + bool _respawnCompatibilityMode{true}; uint32 m_spellId; time_t m_respawnTime; // (secs) time of next respawn (or despawn if GO have owner()), uint32 m_respawnDelayTime; // (secs) if 0 then current GO state no dependent from timer diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index f1e100bba..9319a50d4 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -311,8 +311,9 @@ ObjectMgr::ObjectMgr(): _playerClassInfo[i] = nullptr; } - // Initialize default spawn group - _spawnGroupDataStore[0] = {0, "Default Group", 0, SpawnGroupFlags(SPAWNGROUP_FLAG_SYSTEM)}; + // Initialize default spawn groups + _spawnGroupDataStore[0] = {0, "Default Group", SPAWNGROUP_MAP_UNSET, SpawnGroupFlags(SPAWNGROUP_FLAG_SYSTEM)}; + _spawnGroupDataStore[1] = {1, "Legacy Group", SPAWNGROUP_MAP_UNSET, SpawnGroupFlags(SPAWNGROUP_FLAG_SYSTEM | SPAWNGROUP_FLAG_COMPATIBILITY_MODE)}; } ObjectMgr::~ObjectMgr() @@ -2380,6 +2381,7 @@ void ObjectMgr::LoadCreatures() continue; } CreatureData& data = _creatureDataStore[spawnId]; + data.spawnId = spawnId; data.id1 = id1; data.id2 = id2; data.id3 = id3; @@ -2748,6 +2750,7 @@ ObjectGuid::LowType ObjectMgr::AddGOData(uint32 entry, uint32 mapId, float x, fl ObjectGuid::LowType spawnId = GenerateGameObjectSpawnId(); GameObjectData& data = NewGOData(spawnId); + data.spawnId = spawnId; data.id = entry; data.mapid = mapId; data.posX = x; @@ -2801,6 +2804,7 @@ ObjectGuid::LowType ObjectMgr::AddCreData(uint32 entry, uint32 mapId, float x, f ObjectGuid::LowType spawnId = GenerateCreatureSpawnId(); CreatureData& data = NewOrExistCreatureData(spawnId); + data.spawnId = spawnId; data.spawnMask = spawnId; data.id1 = entry; data.id2 = 0; @@ -2909,6 +2913,7 @@ void ObjectMgr::LoadGameobjects() GameObjectData& data = _gameObjectDataStore[guid]; + data.spawnId = guid; data.id = entry; data.mapid = fields[2].Get(); data.posX = fields[3].Get(); @@ -8703,6 +8708,172 @@ SpawnData const* ObjectMgr::GetSpawnData(SpawnObjectType type, ObjectGuid::LowTy } } +void ObjectMgr::LoadSpawnGroupTemplates() +{ + uint32 oldMSTime = getMSTime(); + + _spawnGroupDataStore.clear(); + + // 0 1 2 + QueryResult result = WorldDatabase.Query("SELECT groupId, groupName, groupFlags FROM spawn_group_template"); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 groupId = fields[0].Get(); + SpawnGroupTemplateData& group = _spawnGroupDataStore[groupId]; + group.groupId = groupId; + group.name = fields[1].Get(); + group.mapId = SPAWNGROUP_MAP_UNSET; + uint32 flags = fields[2].Get(); + if (flags & ~uint32(SPAWNGROUP_FLAG_ALL)) + { + flags &= uint32(SPAWNGROUP_FLAG_ALL); + LOG_ERROR("sql.sql", "Invalid spawn group flag {} on group ID {} ({}), reduced to valid flags {}.", + fields[2].Get(), groupId, group.name, flags); + } + if ((flags & SPAWNGROUP_FLAG_SYSTEM) && (flags & SPAWNGROUP_FLAG_MANUAL_SPAWN)) + { + flags &= ~SPAWNGROUP_FLAG_MANUAL_SPAWN; + LOG_ERROR("sql.sql", "System spawn group {} ({}) has invalid manual spawn flag. Ignored.", groupId, group.name); + } + group.flags = SpawnGroupFlags(flags); + } while (result->NextRow()); + } + + if (_spawnGroupDataStore.find(0) == _spawnGroupDataStore.end()) + { + LOG_ERROR("sql.sql", "Default spawn group (index 0) is missing from DB! Manually inserted."); + SpawnGroupTemplateData& data = _spawnGroupDataStore[0]; + data.groupId = 0; + data.name = "Default Group"; + data.mapId = SPAWNGROUP_MAP_UNSET; + data.flags = SpawnGroupFlags(SPAWNGROUP_FLAG_SYSTEM); + } + if (_spawnGroupDataStore.find(1) == _spawnGroupDataStore.end()) + { + LOG_ERROR("sql.sql", "Default legacy spawn group (index 1) is missing from DB! Manually inserted."); + SpawnGroupTemplateData& data = _spawnGroupDataStore[1]; + data.groupId = 1; + data.name = "Legacy Group"; + data.mapId = SPAWNGROUP_MAP_UNSET; + data.flags = SpawnGroupFlags(SPAWNGROUP_FLAG_SYSTEM | SPAWNGROUP_FLAG_COMPATIBILITY_MODE); + } + + LOG_INFO("server.loading", ">> Loaded {} spawn group templates in {} ms", _spawnGroupDataStore.size(), GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void ObjectMgr::LoadSpawnGroups() +{ + uint32 oldMSTime = getMSTime(); + + // Reset prior state for hot-reload support + _spawnGroupMapStore.clear(); + for (auto& [id, data] : _creatureDataStore) + data.spawnGroupId = 0; + for (auto& [id, data] : _gameObjectDataStore) + data.spawnGroupId = 0; + + // 0 1 2 + QueryResult result = WorldDatabase.Query("SELECT groupId, spawnType, spawnId FROM spawn_group"); + + if (!result) + { + LOG_INFO("server.loading", ">> Loaded 0 spawn group members. DB table `spawn_group` is empty."); + LOG_INFO("server.loading", " "); + return; + } + + uint32 numMembers = 0; + do + { + Field* fields = result->Fetch(); + uint32 groupId = fields[0].Get(); + uint32 type = fields[1].Get(); + if (type >= SPAWN_TYPE_MAX) + { + LOG_ERROR("sql.sql", "Spawn data with invalid type {} listed for spawn group {}. Skipped.", type, groupId); + continue; + } + SpawnObjectType spawnType = SpawnObjectType(type); + ObjectGuid::LowType spawnId = fields[2].Get(); + + SpawnData const* data = GetSpawnData(spawnType, spawnId); + if (!data) + { + LOG_ERROR("sql.sql", "Spawn data with ID ({},{}) not found, but is listed as a member of spawn group {}!", + uint32(spawnType), spawnId, groupId); + continue; + } + if (data->spawnGroupId) + { + LOG_ERROR("sql.sql", "Spawn with ID ({},{}) is listed as a member of spawn group {}, but is already a member of spawn group {}. Skipping.", + uint32(spawnType), spawnId, groupId, data->spawnGroupId); + continue; + } + + auto it = _spawnGroupDataStore.find(groupId); + if (it == _spawnGroupDataStore.end()) + { + LOG_ERROR("sql.sql", "Spawn group {} assigned to spawn ID ({},{}), but group is not found!", groupId, uint32(spawnType), spawnId); + continue; + } + + SpawnGroupTemplateData& groupTemplate = it->second; + if (groupTemplate.mapId == SPAWNGROUP_MAP_UNSET) + groupTemplate.mapId = data->mapid; + else if (groupTemplate.mapId != data->mapid && !(groupTemplate.flags & SPAWNGROUP_FLAG_SYSTEM)) + { + LOG_ERROR("sql.sql", "Spawn group {} has map ID {}, but spawn ({},{}) has map id {} - spawn NOT added to group!", + groupId, groupTemplate.mapId, uint32(spawnType), spawnId, data->mapid); + continue; + } + + // Warn if spawn is also in a pool (non-system groups and pools are mutually exclusive) + if (!(groupTemplate.flags & SPAWNGROUP_FLAG_SYSTEM)) + { + uint32 poolId = 0; + if (spawnType == SPAWN_TYPE_CREATURE) + poolId = sPoolMgr->IsPartOfAPool(spawnId); + else if (spawnType == SPAWN_TYPE_GAMEOBJECT) + poolId = sPoolMgr->IsPartOfAPool(spawnId); + + if (poolId) + LOG_WARN("sql.sql", "Spawn ({},{}) is a member of spawn group {} and also part of pool {}. This may cause issues!", + uint32(spawnType), spawnId, groupId, poolId); + } + + const_cast(data)->spawnGroupId = groupId; + if (!(groupTemplate.flags & SPAWNGROUP_FLAG_SYSTEM)) + _spawnGroupMapStore.emplace(groupId, data); + ++numMembers; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} spawn group members in {} ms", numMembers, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void ObjectMgr::OnDeleteSpawnData(SpawnData const* data) +{ + auto templateIt = _spawnGroupDataStore.find(data->spawnGroupId); + ASSERT(templateIt != _spawnGroupDataStore.end(), "Spawn data is being deleted and has invalid spawn group index {}!", data->spawnGroupId); + if (templateIt->second.flags & SPAWNGROUP_FLAG_SYSTEM) + return; + + auto pair = _spawnGroupMapStore.equal_range(data->spawnGroupId); + for (auto it = pair.first; it != pair.second; ++it) + { + if (it->second != data) + continue; + _spawnGroupMapStore.erase(it); + return; + } + ASSERT(false, "Spawn data being removed is member of spawn group {}, but not found in lookup table!", data->spawnGroupId); +} + void ObjectMgr::LoadQuestRelationsHelper(QuestRelations& map, std::string const& table, bool starter, bool go) { uint32 oldMSTime = getMSTime(); diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 39bdbe59f..24f44441e 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -507,6 +507,7 @@ typedef std::map LinkedRespawnContainer; typedef std::unordered_map CreatureDataContainer; typedef std::unordered_map GameObjectDataContainer; typedef std::unordered_map SpawnGroupDataContainer; +typedef std::multimap SpawnGroupLinkContainer; typedef std::map > TempSummonDataContainer; typedef std::map > GameObjectSummonDataContainer; typedef std::unordered_map CreatureLocaleContainer; @@ -1040,6 +1041,8 @@ public: void LoadCreatureQuestItems(); void LoadTempSummons(); void LoadGameObjectSummons(); + void LoadSpawnGroupTemplates(); + void LoadSpawnGroups(); void LoadCreatures(); void LoadCreatureSparring(); void LoadLinkedRespawn(); @@ -1277,6 +1280,13 @@ public: auto itr = _spawnGroupDataStore.find(groupId); return itr != _spawnGroupDataStore.end() ? &itr->second : nullptr; } + [[nodiscard]] SpawnGroupTemplateData const* GetDefaultSpawnGroup() const { return &_spawnGroupDataStore.at(0); } + [[nodiscard]] SpawnGroupTemplateData const* GetLegacySpawnGroup() const { return &_spawnGroupDataStore.at(1); } + std::pair GetSpawnDataForGroup(uint32 groupId) const + { + return _spawnGroupMapStore.equal_range(groupId); + } + void OnDeleteSpawnData(SpawnData const* data); [[nodiscard]] CreatureLocale const* GetCreatureLocale(uint32 entry) const { @@ -1653,6 +1663,7 @@ private: CreatureLocaleContainer _creatureLocaleStore; GameObjectDataContainer _gameObjectDataStore; SpawnGroupDataContainer _spawnGroupDataStore; + SpawnGroupLinkContainer _spawnGroupMapStore; GameObjectLocaleContainer _gameObjectLocaleStore; GameObjectTemplateContainer _gameObjectTemplateStore; GameObjectTemplateAddonContainer _gameObjectTemplateAddonStore; diff --git a/src/server/game/Grids/GridObjectLoader.cpp b/src/server/game/Grids/GridObjectLoader.cpp index 07a5d2211..a0a82fe4b 100644 --- a/src/server/game/Grids/GridObjectLoader.cpp +++ b/src/server/game/Grids/GridObjectLoader.cpp @@ -22,6 +22,7 @@ #include "DynamicObject.h" #include "GameObject.h" #include "GridNotifiers.h" +#include "ObjectMgr.h" #include "Transport.h" template @@ -38,6 +39,11 @@ void GridObjectLoader::LoadCreatures(CellGuidSet const& guid_set, Map* map) { for (ObjectGuid::LowType const& guid : guid_set) { + // Skip spawns whose spawn group is not active on this map + CreatureData const* cData = sObjectMgr->GetCreatureData(guid); + if (cData && !map->IsSpawnGroupActive(cData->spawnGroupId)) + continue; + Creature* obj = new Creature(); if (!obj->LoadFromDB(guid, map)) { @@ -65,6 +71,10 @@ void GridObjectLoader::LoadGameObjects(CellGuidSet const& guid_set, Map* map) { GameObjectData const* data = sObjectMgr->GetGameObjectData(guid); + // Skip spawns whose spawn group is not active on this map + if (data && !map->IsSpawnGroupActive(data->spawnGroupId)) + continue; + if (data && sObjectMgr->IsGameObjectStaticTransport(data->id)) { StaticTransport* transport = new StaticTransport(); diff --git a/src/server/game/Maps/Map.cpp b/src/server/game/Maps/Map.cpp index 3e37ef92a..57b18d5f4 100644 --- a/src/server/game/Maps/Map.cpp +++ b/src/server/game/Maps/Map.cpp @@ -36,6 +36,7 @@ #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Pet.h" +#include "PoolMgr.h" #include "ScriptMgr.h" #include "Transport.h" #include "VMapFactory.h" @@ -457,6 +458,18 @@ void Map::Update(const uint32 t_diff, const uint32 s_diff, bool /*thread*/) return; } + /// Process any due respawns (non-compatibility mode spawns) + if (!sWorld->getBoolConfig(CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE)) + { + if (_respawnCheckTimer <= t_diff) + { + ProcessRespawns(); + _respawnCheckTimer = 5000; // Check every 5 seconds + } + else + _respawnCheckTimer -= t_diff; + } + _updatableObjectListRecheckTimer.Update(t_diff); resetMarkedCells(); @@ -2382,7 +2395,13 @@ void Map::SaveCreatureRespawnTime(ObjectGuid::LowType spawnId, time_t& respawnTi if (GetInstanceResetPeriod() > 0 && respawnTime - now + 5 >= GetInstanceResetPeriod()) respawnTime = now + YEAR; + // Remove old queue entry if updating an existing respawn time + auto itr = _creatureRespawnTimes.find(spawnId); + if (itr != _creatureRespawnTimes.end()) + _respawnQueue.erase({itr->second, SPAWN_TYPE_CREATURE, spawnId}); + _creatureRespawnTimes[spawnId] = respawnTime; + _respawnQueue.insert({respawnTime, SPAWN_TYPE_CREATURE, spawnId}); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CREATURE_RESPAWN); stmt->SetData(0, spawnId); @@ -2394,7 +2413,12 @@ void Map::SaveCreatureRespawnTime(ObjectGuid::LowType spawnId, time_t& respawnTi void Map::RemoveCreatureRespawnTime(ObjectGuid::LowType spawnId) { - _creatureRespawnTimes.erase(spawnId); + auto itr = _creatureRespawnTimes.find(spawnId); + if (itr != _creatureRespawnTimes.end()) + { + _respawnQueue.erase({itr->second, SPAWN_TYPE_CREATURE, spawnId}); + _creatureRespawnTimes.erase(itr); + } CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CREATURE_RESPAWN); stmt->SetData(0, spawnId); @@ -2416,7 +2440,13 @@ void Map::SaveGORespawnTime(ObjectGuid::LowType spawnId, time_t& respawnTime) if (GetInstanceResetPeriod() > 0 && respawnTime - now + 5 >= GetInstanceResetPeriod()) respawnTime = now + YEAR; + // Remove old queue entry if updating an existing respawn time + auto itr = _goRespawnTimes.find(spawnId); + if (itr != _goRespawnTimes.end()) + _respawnQueue.erase({itr->second, SPAWN_TYPE_GAMEOBJECT, spawnId}); + _goRespawnTimes[spawnId] = respawnTime; + _respawnQueue.insert({respawnTime, SPAWN_TYPE_GAMEOBJECT, spawnId}); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_GO_RESPAWN); stmt->SetData(0, spawnId); @@ -2428,7 +2458,12 @@ void Map::SaveGORespawnTime(ObjectGuid::LowType spawnId, time_t& respawnTime) void Map::RemoveGORespawnTime(ObjectGuid::LowType spawnId) { - _goRespawnTimes.erase(spawnId); + auto itr = _goRespawnTimes.find(spawnId); + if (itr != _goRespawnTimes.end()) + { + _respawnQueue.erase({itr->second, SPAWN_TYPE_GAMEOBJECT, spawnId}); + _goRespawnTimes.erase(itr); + } CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_GO_RESPAWN); stmt->SetData(0, spawnId); @@ -2448,9 +2483,10 @@ void Map::LoadRespawnTimes() { Field* fields = result->Fetch(); ObjectGuid::LowType lowguid = fields[0].Get(); - uint32 respawnTime = fields[1].Get(); + time_t respawnTime = time_t(fields[1].Get()); - _creatureRespawnTimes[lowguid] = time_t(respawnTime); + _creatureRespawnTimes[lowguid] = respawnTime; + _respawnQueue.insert({respawnTime, SPAWN_TYPE_CREATURE, lowguid}); } while (result->NextRow()); } @@ -2463,9 +2499,10 @@ void Map::LoadRespawnTimes() { Field* fields = result->Fetch(); ObjectGuid::LowType lowguid = fields[0].Get(); - uint32 respawnTime = fields[1].Get(); + time_t respawnTime = time_t(fields[1].Get()); - _goRespawnTimes[lowguid] = time_t(respawnTime); + _goRespawnTimes[lowguid] = respawnTime; + _respawnQueue.insert({respawnTime, SPAWN_TYPE_GAMEOBJECT, lowguid}); } while (result->NextRow()); } } @@ -2474,6 +2511,7 @@ void Map::DeleteRespawnTimes() { _creatureRespawnTimes.clear(); _goRespawnTimes.clear(); + _respawnQueue.clear(); DeleteRespawnTimesInDB(GetId(), GetInstanceId()); } @@ -2491,6 +2529,309 @@ void Map::DeleteRespawnTimesInDB(uint16 mapId, uint32 instanceId) CharacterDatabase.Execute(stmt); } +bool Map::IsSpawnGroupActive(uint32 groupId) const +{ + SpawnGroupTemplateData const* data = sObjectMgr->GetSpawnGroupData(groupId); + if (!data) + return false; + + // System groups are always active + if (data->flags & SPAWNGROUP_FLAG_SYSTEM) + return true; + + // Per-map toggled state: XOR with default. + // MANUAL_SPAWN groups default to inactive; toggling makes them active. + // Non-MANUAL groups default to active; toggling makes them inactive. + bool toggled = _toggledSpawnGroupIds.count(groupId) != 0; + bool defaultActive = !(data->flags & SPAWNGROUP_FLAG_MANUAL_SPAWN); + return toggled != defaultActive; // XOR: toggled flips the default +} + +bool Map::SpawnGroupSpawn(uint32 groupId, bool ignoreRespawn /*= false*/, bool force /*= false*/) +{ + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData || (groupData->flags & SPAWNGROUP_FLAG_SYSTEM)) + { + LOG_ERROR("maps", "Tried to spawn non-existing (or system) spawn group {}. Blocked.", groupId); + return false; + } + + if (groupData->mapId != SPAWNGROUP_MAP_UNSET && groupData->mapId != GetId()) + { + LOG_ERROR("maps", "Tried to spawn group {} on map {}, but group has map {}. Blocked.", + groupId, GetId(), groupData->mapId); + return false; + } + + // Mark group as active on this map (toggle to active state) + if (groupData->flags & SPAWNGROUP_FLAG_MANUAL_SPAWN) + _toggledSpawnGroupIds.insert(groupId); + else + _toggledSpawnGroupIds.erase(groupId); + + auto range = sObjectMgr->GetSpawnDataForGroup(groupId); + for (auto it = range.first; it != range.second; ++it) + { + SpawnData const* data = it->second; + ObjectGuid::LowType spawnId = data->spawnId; + + // Check if there's already an alive instance + if (!force) + { + if (data->type == SPAWN_TYPE_CREATURE) + { + auto bounds = _creatureBySpawnIdStore.equal_range(spawnId); + bool alive = false; + for (auto itr = bounds.first; itr != bounds.second; ++itr) + if (itr->second->IsAlive()) + alive = true; + if (alive) + continue; + } + else if (data->type == SPAWN_TYPE_GAMEOBJECT) + { + if (_gameobjectBySpawnIdStore.count(spawnId)) + continue; + } + } + + time_t respawnTime = GetRespawnTime(data->type, spawnId); + if (respawnTime && respawnTime > GameTime::GetGameTime().count()) + { + if (!force && !ignoreRespawn) + continue; + RemoveRespawnTime(data->type, spawnId); + } + + // Don't spawn if grid isn't loaded (will be handled in grid loader) + if (!IsGridLoaded(data->posX, data->posY)) + continue; + + switch (data->type) + { + case SPAWN_TYPE_CREATURE: + { + Creature* creature = new Creature(); + if (!creature->LoadCreatureFromDB(spawnId, this, true, true)) + delete creature; + break; + } + case SPAWN_TYPE_GAMEOBJECT: + { + GameObject* gameobject = new GameObject(); + if (!gameobject->LoadGameObjectFromDB(spawnId, this, true)) + delete gameobject; + break; + } + default: + break; + } + } + + return true; +} + +bool Map::SpawnGroupDespawn(uint32 groupId, bool deleteRespawnTimes /*= false*/) +{ + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData || (groupData->flags & SPAWNGROUP_FLAG_SYSTEM)) + { + LOG_ERROR("maps", "Tried to despawn non-existing (or system) spawn group {}. Blocked.", groupId); + return false; + } + + if (groupData->mapId != SPAWNGROUP_MAP_UNSET && groupData->mapId != GetId()) + { + LOG_ERROR("maps", "Tried to despawn group {} on map {}, but group has map {}. Blocked.", + groupId, GetId(), groupData->mapId); + return false; + } + + // Mark group as inactive on this map (toggle to inactive state) + if (groupData->flags & SPAWNGROUP_FLAG_MANUAL_SPAWN) + _toggledSpawnGroupIds.erase(groupId); + else + _toggledSpawnGroupIds.insert(groupId); + + std::vector toUnload; + auto range = sObjectMgr->GetSpawnDataForGroup(groupId); + for (auto it = range.first; it != range.second; ++it) + { + SpawnData const* data = it->second; + ObjectGuid::LowType spawnId = data->spawnId; + + if (deleteRespawnTimes) + RemoveRespawnTime(data->type, spawnId); + + switch (data->type) + { + case SPAWN_TYPE_CREATURE: + { + auto bounds = _creatureBySpawnIdStore.equal_range(spawnId); + for (auto itr = bounds.first; itr != bounds.second; ++itr) + toUnload.emplace_back(itr->second); + break; + } + case SPAWN_TYPE_GAMEOBJECT: + { + auto bounds = _gameobjectBySpawnIdStore.equal_range(spawnId); + for (auto itr = bounds.first; itr != bounds.second; ++itr) + toUnload.emplace_back(itr->second); + break; + } + default: + break; + } + } + + for (WorldObject* obj : toUnload) + obj->AddObjectToRemoveList(); + + return true; +} + +void Map::ProcessRespawns() +{ + time_t now = GameTime::GetGameTime().count(); + + // Process due respawns from the time-ordered queue. + // Entries are sorted by respawnTime — once we hit a future time, we're done. + while (!_respawnQueue.empty()) + { + auto it = _respawnQueue.begin(); + if (it->respawnTime > now) + break; // nothing else is due this tick + + SpawnObjectType type = it->type; + ObjectGuid::LowType spawnId = it->spawnId; + + // Remove from queue first — handlers below call Remove*RespawnTime() + // which also erases from queue, so we must pop before processing. + _respawnQueue.erase(it); + + if (type == SPAWN_TYPE_CREATURE) + ProcessCreatureRespawn(spawnId); + else if (type == SPAWN_TYPE_GAMEOBJECT) + ProcessGameObjectRespawn(spawnId); + } +} + +void Map::ProcessCreatureRespawn(ObjectGuid::LowType spawnId) +{ + // Pool members are handled entirely by PoolMgr + if (uint32 poolId = sPoolMgr->IsPartOfAPool(spawnId)) + { + sPoolMgr->UpdatePool(poolId, spawnId); + RemoveCreatureRespawnTime(spawnId); + return; + } + + CreatureData const* data = sObjectMgr->GetCreatureData(spawnId); + if (!data) + { + RemoveCreatureRespawnTime(spawnId); + return; + } + + // Compat-mode creatures handle their own respawn in-place — don't interfere. + // Clean up the stale respawn time entry since the legacy system manages these. + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(data->spawnGroupId); + if (!groupData || (groupData->flags & SPAWNGROUP_FLAG_COMPATIBILITY_MODE)) + { + RemoveCreatureRespawnTime(spawnId); + return; + } + + // Don't respawn if the spawn group is not active + if (!IsSpawnGroupActive(data->spawnGroupId)) + { + // Re-queue — will be checked again next ProcessRespawns() tick + _respawnQueue.insert({GameTime::GetGameTime().count() + 5, SPAWN_TYPE_CREATURE, spawnId}); + return; + } + + // Skip if grid isn't loaded (will be handled in grid loader) + if (!IsGridLoaded(data->posX, data->posY)) + { + _respawnQueue.insert({GameTime::GetGameTime().count() + 5, SPAWN_TYPE_CREATURE, spawnId}); + return; + } + + // Skip if already alive + auto bounds = _creatureBySpawnIdStore.equal_range(spawnId); + for (auto itr = bounds.first; itr != bounds.second; ++itr) + { + if (itr->second->IsAlive()) + { + RemoveCreatureRespawnTime(spawnId); + return; + } + } + + // Remove respawn time BEFORE LoadFromDB, otherwise the creature + // reads it back and loads as DEAD instead of ALIVE + RemoveCreatureRespawnTime(spawnId); + + Creature* creature = new Creature(); + if (!creature->LoadCreatureFromDB(spawnId, this, true, true)) + delete creature; +} + +void Map::ProcessGameObjectRespawn(ObjectGuid::LowType spawnId) +{ + // Pool members are handled entirely by PoolMgr + if (uint32 poolId = sPoolMgr->IsPartOfAPool(spawnId)) + { + sPoolMgr->UpdatePool(poolId, spawnId); + RemoveGORespawnTime(spawnId); + return; + } + + GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId); + if (!data) + { + RemoveGORespawnTime(spawnId); + return; + } + + // Compat-mode gameobjects handle their own respawn — don't interfere. + // Clean up the stale respawn time entry since the legacy system manages these. + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(data->spawnGroupId); + if (!groupData || (groupData->flags & SPAWNGROUP_FLAG_COMPATIBILITY_MODE)) + { + RemoveGORespawnTime(spawnId); + return; + } + + // Don't respawn if the spawn group is not active + if (!IsSpawnGroupActive(data->spawnGroupId)) + { + _respawnQueue.insert({GameTime::GetGameTime().count() + 5, SPAWN_TYPE_GAMEOBJECT, spawnId}); + return; + } + + // Skip if grid isn't loaded (will be handled in grid loader) + if (!IsGridLoaded(data->posX, data->posY)) + { + _respawnQueue.insert({GameTime::GetGameTime().count() + 5, SPAWN_TYPE_GAMEOBJECT, spawnId}); + return; + } + + if (_gameobjectBySpawnIdStore.count(spawnId)) + { + RemoveGORespawnTime(spawnId); + return; + } + + // Remove respawn time BEFORE LoadFromDB, otherwise the GO + // reads it back and loads as despawned + RemoveGORespawnTime(spawnId); + + GameObject* gameobject = new GameObject(); + if (!gameobject->LoadGameObjectFromDB(spawnId, this, true)) + delete gameobject; +} + void Map::UpdateEncounterState(EncounterCreditType type, uint32 creditEntry, Unit* source) { Difficulty difficulty_fixed = (IsSharedDifficultyMap(GetId()) ? Difficulty(GetDifficulty() % 2) : GetDifficulty()); diff --git a/src/server/game/Maps/Map.h b/src/server/game/Maps/Map.h index bd3abd228..e91378040 100644 --- a/src/server/game/Maps/Map.h +++ b/src/server/game/Maps/Map.h @@ -35,11 +35,13 @@ #include "PathGenerator.h" #include "Position.h" #include "SharedDefines.h" +#include "SpawnData.h" #include "Timer.h" #include "GridTerrainData.h" #include #include #include +#include #include class Unit; @@ -424,6 +426,8 @@ public: void RemoveCreatureRespawnTime(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; } + [[nodiscard]] std::unordered_map const& GetGORespawnTimes() const { return _goRespawnTimes; } void LoadRespawnTimes(); void DeleteRespawnTimes(); [[nodiscard]] time_t GetInstanceResetPeriod() const { return _instanceResetPeriod; } @@ -431,6 +435,40 @@ public: void UpdatePlayerZoneStats(uint32 oldZone, uint32 newZone); [[nodiscard]] uint32 ApplyDynamicModeRespawnScaling(WorldObject const* obj, uint32 respawnDelay) const; + bool SpawnGroupSpawn(uint32 groupId, bool ignoreRespawn = false, bool force = false); + bool SpawnGroupDespawn(uint32 groupId, bool deleteRespawnTimes = false); + [[nodiscard]] bool IsSpawnGroupActive(uint32 groupId) const; + void ProcessRespawns(); + void ProcessCreatureRespawn(ObjectGuid::LowType spawnId); + void ProcessGameObjectRespawn(ObjectGuid::LowType spawnId); + + [[nodiscard]] time_t GetRespawnTime(SpawnObjectType type, ObjectGuid::LowType spawnId) const + { + switch (type) + { + case SPAWN_TYPE_CREATURE: + return GetCreatureRespawnTime(spawnId); + case SPAWN_TYPE_GAMEOBJECT: + return GetGORespawnTime(spawnId); + default: + return time_t(0); + } + } + void RemoveRespawnTime(SpawnObjectType type, ObjectGuid::LowType spawnId) + { + switch (type) + { + case SPAWN_TYPE_CREATURE: + RemoveCreatureRespawnTime(spawnId); + break; + case SPAWN_TYPE_GAMEOBJECT: + RemoveGORespawnTime(spawnId); + break; + default: + break; + } + } + EventProcessor Events; void ScheduleCreatureRespawn(ObjectGuid /*creatureGuid*/, Milliseconds /*respawnTimer*/, Position pos = Position()); @@ -602,6 +640,27 @@ private: std::unordered_map _creatureRespawnTimes; std::unordered_map _goRespawnTimes; + // Time-ordered index for ProcessRespawns() — avoids O(n) full scan. + // Based on TrinityCore's priority queue approach (r00ty-tc, 59db2eee). + struct RespawnEntry + { + time_t respawnTime; + SpawnObjectType type; + ObjectGuid::LowType spawnId; + bool operator<(RespawnEntry const& other) const + { + if (respawnTime != other.respawnTime) + return respawnTime < other.respawnTime; + if (type != other.type) + return type < other.type; + return spawnId < other.spawnId; + } + }; + std::set _respawnQueue; + + std::unordered_set _toggledSpawnGroupIds; + uint32 _respawnCheckTimer{0}; + std::unordered_map _zonePlayerCountMap; ZoneDynamicInfoMap _zoneDynamicInfo; diff --git a/src/server/game/Maps/SpawnData.h b/src/server/game/Maps/SpawnData.h index b1136203d..8819db107 100644 --- a/src/server/game/Maps/SpawnData.h +++ b/src/server/game/Maps/SpawnData.h @@ -19,6 +19,7 @@ #define AZEROTHCORE_SPAWNDATA_H #include "Define.h" +#include "ObjectGuid.h" #include enum SpawnObjectType : uint8 @@ -41,26 +42,30 @@ enum SpawnGroupFlags : uint32 { SPAWNGROUP_FLAG_NONE = 0x00, SPAWNGROUP_FLAG_SYSTEM = 0x01, + SPAWNGROUP_FLAG_COMPATIBILITY_MODE = 0x02, SPAWNGROUP_FLAG_MANUAL_SPAWN = 0x04, SPAWNGROUP_FLAG_DYNAMIC_SPAWN_RATE = 0x08, SPAWNGROUP_FLAG_ESCORTQUESTNPC = 0x10, - SPAWNGROUP_FLAG_ALL = SPAWNGROUP_FLAG_SYSTEM | + SPAWNGROUP_FLAG_ALL = SPAWNGROUP_FLAG_SYSTEM | SPAWNGROUP_FLAG_COMPATIBILITY_MODE | SPAWNGROUP_FLAG_MANUAL_SPAWN | SPAWNGROUP_FLAG_DYNAMIC_SPAWN_RATE | SPAWNGROUP_FLAG_ESCORTQUESTNPC }; +constexpr uint32 SPAWNGROUP_MAP_UNSET = 0xFFFFFFFF; + struct SpawnGroupTemplateData { uint32 groupId; std::string name; - uint16 mapid; + uint32 mapId; SpawnGroupFlags flags; }; struct SpawnData { SpawnObjectType const type; + ObjectGuid::LowType spawnId{0}; uint16 mapid{0}; uint32 phaseMask{0}; float posX{0.0f}; diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 2172cf6ec..8ecc58b47 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1449,6 +1449,32 @@ enum AcoreStrings // Achievement commands LANG_ACHIEVEMENT_ADD_ONLINE = 30126, - LANG_ACHIEVEMENT_ADD_OFFLINE = 30127 + LANG_ACHIEVEMENT_ADD_OFFLINE = 30127, + + // Spawn group commands + LANG_SPAWNGROUP_SPAWN_SYSTEM_ERROR = 35411, + LANG_SPAWNGROUP_SPAWN_SUCCESS = 35412, + LANG_SPAWNGROUP_SPAWN_FAILED = 35413, + LANG_SPAWNGROUP_DESPAWN_SYSTEM_ERROR = 35414, + LANG_SPAWNGROUP_DESPAWN_SUCCESS = 35415, + LANG_SPAWNGROUP_DESPAWN_FAILED = 35416, + LANG_LIST_RESPAWNS_CREATURE_HEADER = 35419, + LANG_LIST_RESPAWNS_CREATURE_ENTRY = 35420, + LANG_LIST_RESPAWNS_GO_HEADER = 35421, + LANG_LIST_RESPAWNS_GO_ENTRY = 35422, + LANG_LIST_RESPAWNS_LIMIT = 35423, + LANG_SPAWNGROUP_NOT_FOUND = 35424, + + // Pool debug commands + LANG_POOL_NOT_FOUND = 35425, + LANG_POOL_INFO_HEADER = 35426, + LANG_POOL_INFO_MEMBERS_HEADER = 35427, + LANG_POOL_INFO_MEMBER = 35428, + LANG_POOL_INFO_SUBPOOLS_HEADER = 35429, + LANG_POOL_INFO_SUBPOOL = 35430, + LANG_POOL_LOOKUP_IN_POOL = 35431, + LANG_POOL_LOOKUP_NOT_IN_POOL = 35432, + LANG_POOL_LOOKUP_USE_INFO = 35433, + LANG_POOL_LOOKUP_NOTARGET = 35434 }; #endif diff --git a/src/server/game/Pools/PoolMgr.cpp b/src/server/game/Pools/PoolMgr.cpp index efcd9e360..0a22e2e9d 100644 --- a/src/server/game/Pools/PoolMgr.cpp +++ b/src/server/game/Pools/PoolMgr.cpp @@ -580,7 +580,7 @@ void PoolMgr::LoadFromDB() { uint32 oldMSTime = getMSTime(); - QueryResult result = WorldDatabase.Query("SELECT entry, max_limit FROM pool_template"); + QueryResult result = WorldDatabase.Query("SELECT entry, max_limit, description FROM pool_template"); if (!result) { mPoolTemplate.clear(); @@ -597,7 +597,8 @@ void PoolMgr::LoadFromDB() uint32 pool_id = fields[0].Get(); PoolTemplateData& pPoolTemplate = mPoolTemplate[pool_id]; - pPoolTemplate.MaxLimit = fields[1].Get(); + pPoolTemplate.MaxLimit = fields[1].Get(); + pPoolTemplate.Description = fields[2].Get(); ++count; } while (result->NextRow()); @@ -1166,3 +1167,39 @@ template void PoolMgr::UpdatePool(uint32 pool_id, uint32 db_guid_or_pool_i template void PoolMgr::UpdatePool(uint32 pool_id, uint32 db_guid_or_pool_id); template void PoolMgr::UpdatePool(uint32 pool_id, uint32 db_guid_or_pool_id); template void PoolMgr::UpdatePool(uint32 pool_id, uint32 db_guid_or_pool_id); + +PoolTemplateData const* PoolMgr::GetPoolTemplate(uint32 poolId) const +{ + auto itr = mPoolTemplate.find(poolId); + return itr != mPoolTemplate.end() ? &itr->second : nullptr; +} + +PoolGroup const* PoolMgr::GetPoolCreatureGroup(uint32 poolId) const +{ + auto itr = mPoolCreatureGroups.find(poolId); + return itr != mPoolCreatureGroups.end() ? &itr->second : nullptr; +} + +PoolGroup const* PoolMgr::GetPoolGameObjectGroup(uint32 poolId) const +{ + auto itr = mPoolGameobjectGroups.find(poolId); + return itr != mPoolGameobjectGroups.end() ? &itr->second : nullptr; +} + +PoolGroup const* PoolMgr::GetPoolPoolGroup(uint32 poolId) const +{ + auto itr = mPoolPoolGroups.find(poolId); + return itr != mPoolPoolGroups.end() ? &itr->second : nullptr; +} + +uint32 PoolMgr::GetCreaturePoolId(uint32 guid) const +{ + SearchMap::const_iterator itr = mCreatureSearchMap.find(guid); + return itr != mCreatureSearchMap.end() ? itr->second : 0; +} + +uint32 PoolMgr::GetGameObjectPoolId(uint32 guid) const +{ + SearchMap::const_iterator itr = mGameobjectSearchMap.find(guid); + return itr != mGameobjectSearchMap.end() ? itr->second : 0; +} diff --git a/src/server/game/Pools/PoolMgr.h b/src/server/game/Pools/PoolMgr.h index 447d3a5be..ab0199d71 100644 --- a/src/server/game/Pools/PoolMgr.h +++ b/src/server/game/Pools/PoolMgr.h @@ -25,7 +25,8 @@ struct PoolTemplateData { - uint32 MaxLimit; + uint32 MaxLimit; + std::string Description; }; struct PoolObject @@ -89,6 +90,8 @@ public: return EqualChanced.front().guid; } uint32 GetPoolId() const { return poolId; } + std::vector const& GetExplicitlyChanced() const { return ExplicitlyChanced; } + std::vector const& GetEqualChanced() const { return EqualChanced; } private: uint32 poolId; PoolObjectList ExplicitlyChanced; @@ -135,6 +138,15 @@ public: PooledQuestRelation mQuestCreatureRelation; PooledQuestRelation mQuestGORelation; + // Pool info accessors for debug commands + PoolTemplateData const* GetPoolTemplate(uint32 poolId) const; + PoolGroup const* GetPoolCreatureGroup(uint32 poolId) const; + PoolGroup const* GetPoolGameObjectGroup(uint32 poolId) const; + PoolGroup const* GetPoolPoolGroup(uint32 poolId) const; + ActivePoolData const& GetSpawnedData() const { return mSpawnedData; } + uint32 GetCreaturePoolId(uint32 guid) const; + uint32 GetGameObjectPoolId(uint32 guid) const; + friend class PoolQuestReloadFixTest; private: template diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index ffaf405cb..72b8004e0 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -553,6 +553,9 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Creature Base Stats..."); sObjectMgr->LoadCreatureClassLevelStats(); + LOG_INFO("server.loading", "Loading Spawn Group Templates..."); + sObjectMgr->LoadSpawnGroupTemplates(); + LOG_INFO("server.loading", "Loading Creature Data..."); sObjectMgr->LoadCreatures(); @@ -580,6 +583,9 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Gameobject Data..."); sObjectMgr->LoadGameobjects(); + LOG_INFO("server.loading", "Loading Spawn Group Data..."); + sObjectMgr->LoadSpawnGroups(); // must be after LoadCreatures() and LoadGameobjects() + LOG_INFO("server.loading", "Loading GameObject Addon Data..."); sObjectMgr->LoadGameObjectAddons(); // must be after LoadGameObjectTemplate() and LoadGameobjects() diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 530cef16c..f637cc372 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -521,6 +521,8 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_RESPAWN_DYNAMICRATE_GAMEOBJECT, "Respawn.DynamicRateGameObject", 1.0f); SetConfigValue(CONFIG_RESPAWN_DYNAMICMINIMUM_GAMEOBJECT, "Respawn.DynamicMinimumGameObject", 10); + SetConfigValue(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC, "Respawn.DynamicEscortNPC", false); + SetConfigValue(CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE, "Respawn.ForceCompatibilityMode", false); SetConfigValue(CONFIG_VMAP_INDOOR_CHECK, "vmap.enableIndoorCheck", true); SetConfigValue(CONFIG_VMAP_ENABLE_LOS, "vmap.enableLOS", true); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index 66709d66b..499baba6c 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -391,6 +391,8 @@ enum ServerConfigs CONFIG_SCOURGEINVASION_COUNTER_THIRD, CONFIG_RESPAWN_DYNAMICMINIMUM_GAMEOBJECT, CONFIG_RESPAWN_DYNAMICMINIMUM_CREATURE, + CONFIG_RESPAWN_DYNAMIC_ESCORTNPC, + CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE, RATE_HEALTH, RATE_POWER_MANA, RATE_POWER_RAGE_INCOME, diff --git a/src/server/scripts/Commands/cs_gobject.cpp b/src/server/scripts/Commands/cs_gobject.cpp index 50ccde003..79ce132bd 100644 --- a/src/server/scripts/Commands/cs_gobject.cpp +++ b/src/server/scripts/Commands/cs_gobject.cpp @@ -57,7 +57,9 @@ public: { "load", HandleGameObjectLoadCommand, SEC_ADMINISTRATOR, Console::Yes }, { "set phase", HandleGameObjectSetPhaseCommand, SEC_ADMINISTRATOR, Console::No }, { "set state", HandleGameObjectSetStateCommand, SEC_ADMINISTRATOR, Console::No }, - { "respawn", HandleGameObjectRespawn, SEC_GAMEMASTER, Console::No } + { "respawn", HandleGameObjectRespawn, SEC_GAMEMASTER, Console::No }, + { "spawngroup", HandleGameObjectSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No }, + { "despawngroup", HandleGameObjectDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No } }; static ChatCommandTable commandTable = { @@ -688,6 +690,60 @@ public: handler->PSendSysMessage(LANG_CMD_GO_RESPAWN, object->GetNameForLocaleIdx(handler->GetSessionDbcLocale()), object->GetEntry(), object->GetSpawnId()); return true; } + + static bool HandleGameObjectSpawnGroupCommand(ChatHandler* handler, uint32 groupId) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_NOT_FOUND, groupId); + return false; + } + + if (groupData->flags & SPAWNGROUP_FLAG_SYSTEM) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_SPAWN_SYSTEM_ERROR, groupId, groupData->name); + return false; + } + + if (player->GetMap()->SpawnGroupSpawn(groupId, true, true)) + handler->PSendSysMessage(LANG_SPAWNGROUP_SPAWN_SUCCESS, groupId, groupData->name); + else + handler->SendErrorMessage(LANG_SPAWNGROUP_SPAWN_FAILED, groupId, groupData->name); + + return true; + } + + static bool HandleGameObjectDespawnGroupCommand(ChatHandler* handler, uint32 groupId) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_NOT_FOUND, groupId); + return false; + } + + if (groupData->flags & SPAWNGROUP_FLAG_SYSTEM) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_DESPAWN_SYSTEM_ERROR, groupId, groupData->name); + return false; + } + + if (player->GetMap()->SpawnGroupDespawn(groupId, true)) + handler->PSendSysMessage(LANG_SPAWNGROUP_DESPAWN_SUCCESS, groupId, groupData->name); + else + handler->SendErrorMessage(LANG_SPAWNGROUP_DESPAWN_FAILED, groupId, groupData->name); + + return true; + } }; void AddSC_gobject_commandscript() diff --git a/src/server/scripts/Commands/cs_list.cpp b/src/server/scripts/Commands/cs_list.cpp index 8778d8af7..84b75bd1a 100644 --- a/src/server/scripts/Commands/cs_list.cpp +++ b/src/server/scripts/Commands/cs_list.cpp @@ -20,6 +20,7 @@ #include "Creature.h" #include "DBCStores.h" #include "DatabaseEnv.h" +#include "GameTime.h" #include "GameObject.h" #include "Language.h" #include "MapMgr.h" @@ -45,10 +46,11 @@ public: static ChatCommandTable listCommandTable = { - { "creature", HandleListCreatureCommand, SEC_MODERATOR, Console::Yes }, - { "item", HandleListItemCommand, SEC_MODERATOR, Console::Yes }, - { "object", HandleListObjectCommand, SEC_MODERATOR, Console::Yes }, - { "auras", listAurasCommandTable }, + { "creature", HandleListCreatureCommand, SEC_MODERATOR, Console::Yes }, + { "item", HandleListItemCommand, SEC_MODERATOR, Console::Yes }, + { "object", HandleListObjectCommand, SEC_MODERATOR, Console::Yes }, + { "auras", listAurasCommandTable }, + { "respawns", HandleListRespawnsCommand, SEC_GAMEMASTER, Console::No }, }; static ChatCommandTable commandTable = { @@ -517,6 +519,58 @@ public: return true; } + + static bool HandleListRespawnsCommand(ChatHandler* handler) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + Map* map = player->GetMap(); + uint32 count = 0; + time_t now = GameTime::GetGameTime().count(); + + handler->PSendSysMessage(LANG_LIST_RESPAWNS_CREATURE_HEADER, map->GetId(), map->GetInstanceId()); + for (auto const& pair : map->GetCreatureRespawnTimes()) + { + CreatureData const* data = sObjectMgr->GetCreatureData(pair.first); + if (!data) + continue; + + CreatureTemplate const* cTemplate = sObjectMgr->GetCreatureTemplate(data->id1); + std::string name = cTemplate ? cTemplate->Name : "Unknown"; + time_t remaining = pair.second > now ? pair.second - now : 0; + handler->PSendSysMessage(LANG_LIST_RESPAWNS_CREATURE_ENTRY, pair.first, name, data->id1, remaining); + ++count; + if (count >= 50) + { + handler->SendSysMessage(LANG_LIST_RESPAWNS_LIMIT); + break; + } + } + + count = 0; + handler->SendSysMessage(LANG_LIST_RESPAWNS_GO_HEADER); + for (auto const& pair : map->GetGORespawnTimes()) + { + GameObjectData const* data = sObjectMgr->GetGameObjectData(pair.first); + if (!data) + continue; + + GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(data->id); + std::string name = goTemplate ? goTemplate->name : "Unknown"; + time_t remaining = pair.second > now ? pair.second - now : 0; + handler->PSendSysMessage(LANG_LIST_RESPAWNS_GO_ENTRY, pair.first, name, data->id, remaining); + ++count; + if (count >= 50) + { + handler->SendSysMessage(LANG_LIST_RESPAWNS_LIMIT); + break; + } + } + + return true; + } }; void AddSC_list_commandscript() diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index bc7307086..c28a5de5c 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -40,6 +40,7 @@ #include "ObjectAccessor.h" #include "Pet.h" #include "Player.h" +#include "PoolMgr.h" #include "Realm.h" #include "ScriptMgr.h" #include "SpellAuras.h" @@ -2474,10 +2475,52 @@ public: { Player* player = handler->GetSession()->GetPlayer(); + // Phase 1: respawn creatures/GOs that still have corpses in the grid Acore::RespawnDo u_do; Acore::WorldObjectWorker worker(player, u_do); Cell::VisitObjects(player, worker, player->GetGridActivationRange()); + // Phase 2: force-respawn creatures/GOs that were fully removed (non-compat mode) + // by setting their respawn times to now so ProcessRespawns() picks them up + Map* map = player->GetMap(); + uint32 gridId = Acore::ComputeGridCoord(player->GetPositionX(), player->GetPositionY()).GetId(); + time_t now = GameTime::GetGameTime().count(); + + std::vector creaturesToRespawn; + for (auto const& pair : map->GetCreatureRespawnTimes()) + { + CreatureData const* data = sObjectMgr->GetCreatureData(pair.first); + if (!data || Acore::ComputeGridCoord(data->posX, data->posY).GetId() != gridId) + continue; + + // Skip pooled spawns — Phase 1 already triggered pool rotation via + // Creature::Respawn() -> PoolMgr::UpdatePool(). Forcing a respawn time + // here would cause ProcessRespawns() to call UpdatePool() again, + // spawning duplicates beyond the pool's max_limit. + if (sPoolMgr->IsPartOfAPool(pair.first)) + continue; + + creaturesToRespawn.push_back(pair.first); + } + for (ObjectGuid::LowType spawnId : creaturesToRespawn) + map->SaveCreatureRespawnTime(spawnId, now); + + std::vector goesToRespawn; + for (auto const& pair : map->GetGORespawnTimes()) + { + GameObjectData const* data = sObjectMgr->GetGameObjectData(pair.first); + if (!data || Acore::ComputeGridCoord(data->posX, data->posY).GetId() != gridId) + continue; + + // Skip pooled spawns — same reason as creatures above. + if (sPoolMgr->IsPartOfAPool(pair.first)) + continue; + + goesToRespawn.push_back(pair.first); + } + for (ObjectGuid::LowType spawnId : goesToRespawn) + map->SaveGORespawnTime(spawnId, now); + return true; } diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 62a9468bd..ce70b772e 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -199,7 +199,9 @@ public: { "delete", npcDeleteCommandTable }, { "follow", npcFollowCommandTable }, { "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, Console::Yes }, - { "set", npcSetCommandTable } + { "set", npcSetCommandTable }, + { "spawngroup", HandleNpcSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No }, + { "despawngroup", HandleNpcDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No } }; static ChatCommandTable commandTable = { @@ -1435,6 +1437,60 @@ public: handler->PSendSysMessage("LinkGUID '{}' added to creature with DBTableGUID: '{}'", linkguid, creature->GetSpawnId()); return true; } + + static bool HandleNpcSpawnGroupCommand(ChatHandler* handler, uint32 groupId) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_NOT_FOUND, groupId); + return false; + } + + if (groupData->flags & SPAWNGROUP_FLAG_SYSTEM) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_SPAWN_SYSTEM_ERROR, groupId, groupData->name); + return false; + } + + if (player->GetMap()->SpawnGroupSpawn(groupId, true, true)) + handler->PSendSysMessage(LANG_SPAWNGROUP_SPAWN_SUCCESS, groupId, groupData->name); + else + handler->SendErrorMessage(LANG_SPAWNGROUP_SPAWN_FAILED, groupId, groupData->name); + + return true; + } + + static bool HandleNpcDespawnGroupCommand(ChatHandler* handler, uint32 groupId) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + SpawnGroupTemplateData const* groupData = sObjectMgr->GetSpawnGroupData(groupId); + if (!groupData) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_NOT_FOUND, groupId); + return false; + } + + if (groupData->flags & SPAWNGROUP_FLAG_SYSTEM) + { + handler->SendErrorMessage(LANG_SPAWNGROUP_DESPAWN_SYSTEM_ERROR, groupId, groupData->name); + return false; + } + + if (player->GetMap()->SpawnGroupDespawn(groupId, true)) + handler->PSendSysMessage(LANG_SPAWNGROUP_DESPAWN_SUCCESS, groupId, groupData->name); + else + handler->SendErrorMessage(LANG_SPAWNGROUP_DESPAWN_FAILED, groupId, groupData->name); + + return true; + } }; void AddSC_npc_commandscript() diff --git a/src/server/scripts/Commands/cs_pool.cpp b/src/server/scripts/Commands/cs_pool.cpp new file mode 100644 index 000000000..56addf9f3 --- /dev/null +++ b/src/server/scripts/Commands/cs_pool.cpp @@ -0,0 +1,235 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "Chat.h" +#include "CommandScript.h" +#include "Creature.h" +#include "GameTime.h" +#include "GameObject.h" +#include "Language.h" +#include "MapMgr.h" +#include "ObjectMgr.h" +#include "Player.h" +#include "PoolMgr.h" + +using namespace Acore::ChatCommands; + +class pool_commandscript : public CommandScript +{ +public: + pool_commandscript() : CommandScript("pool_commandscript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable poolCommandTable = + { + { "info", HandlePoolInfoCommand, SEC_GAMEMASTER, Console::Yes }, + { "lookup", HandlePoolLookupCommand, SEC_GAMEMASTER, Console::No }, + }; + static ChatCommandTable commandTable = + { + { "pool", poolCommandTable } + }; + return commandTable; + } + +private: + static char const* StatusTag(bool active) + { + return active ? "ACTIVE" : "inactive"; + } + + static void ListPoolMembers(ChatHandler* handler, char const* typeName, + std::vector const& explicitly, + std::vector const& equal, bool isCreature) + { + if (explicitly.empty() && equal.empty()) + return; + + uint32 total = explicitly.size() + equal.size(); + handler->PSendSysMessage(LANG_POOL_INFO_MEMBERS_HEADER, typeName, total); + + auto printMember = [&](PoolObject const& obj) + { + bool spawned = isCreature + ? sPoolMgr->IsSpawnedObject(obj.guid) + : sPoolMgr->IsSpawnedObject(obj.guid); + + std::string name = "Unknown"; + uint32 entry = 0; + uint32 mapId = 0; + float x = 0, y = 0, z = 0; + + if (isCreature) + { + if (CreatureData const* data = sObjectMgr->GetCreatureData(obj.guid)) + { + entry = data->id1; + mapId = data->mapid; + x = data->posX; + y = data->posY; + z = data->posZ; + if (CreatureTemplate const* tpl = sObjectMgr->GetCreatureTemplate(entry)) + name = tpl->Name; + } + } + else + { + if (GameObjectData const* data = sObjectMgr->GetGameObjectData(obj.guid)) + { + entry = data->id; + mapId = data->mapid; + x = data->posX; + y = data->posY; + z = data->posZ; + if (GameObjectTemplate const* tpl = sObjectMgr->GetGameObjectTemplate(entry)) + name = tpl->name; + } + } + + handler->PSendSysMessage(LANG_POOL_INFO_MEMBER, + StatusTag(spawned), obj.guid, name, entry, mapId, x, y, z); + }; + + for (auto const& obj : explicitly) + printMember(obj); + for (auto const& obj : equal) + printMember(obj); + } + + static bool HandlePoolInfoCommand(ChatHandler* handler, uint32 poolId) + { + PoolTemplateData const* tpl = sPoolMgr->GetPoolTemplate(poolId); + if (!tpl) + { + handler->PSendSysMessage(LANG_POOL_NOT_FOUND, poolId); + handler->SetSentErrorMessage(true); + return false; + } + + uint32 activeCount = sPoolMgr->GetSpawnedData().GetActiveObjectCount(poolId); + handler->PSendSysMessage(LANG_POOL_INFO_HEADER, poolId, + tpl->Description.empty() ? "(none)" : tpl->Description, + tpl->MaxLimit, activeCount); + + // Show parent pool if nested + uint32 parentPool = sPoolMgr->IsPartOfAPool(poolId); + if (parentPool) + handler->PSendSysMessage(" Parent pool: {}", parentPool); + + // Creature members + if (PoolGroup const* creGroup = sPoolMgr->GetPoolCreatureGroup(poolId)) + { + if (!creGroup->IsEmpty()) + ListPoolMembers(handler, "Creatures", + creGroup->GetExplicitlyChanced(), creGroup->GetEqualChanced(), true); + } + + // GameObject members + if (PoolGroup const* goGroup = sPoolMgr->GetPoolGameObjectGroup(poolId)) + { + if (!goGroup->IsEmpty()) + ListPoolMembers(handler, "GameObjects", + goGroup->GetExplicitlyChanced(), goGroup->GetEqualChanced(), false); + } + + // Sub-pool members + if (PoolGroup const* poolGroup = sPoolMgr->GetPoolPoolGroup(poolId)) + { + auto const& explicitly = poolGroup->GetExplicitlyChanced(); + auto const& equal = poolGroup->GetEqualChanced(); + if (!explicitly.empty() || !equal.empty()) + { + uint32 total = explicitly.size() + equal.size(); + handler->PSendSysMessage(LANG_POOL_INFO_SUBPOOLS_HEADER, total); + + auto printSubPool = [&](PoolObject const& obj) + { + bool active = sPoolMgr->GetSpawnedData().IsActiveObject(obj.guid); + PoolTemplateData const* subTpl = sPoolMgr->GetPoolTemplate(obj.guid); + std::string desc = subTpl ? subTpl->Description : "Unknown"; + + handler->PSendSysMessage(LANG_POOL_INFO_SUBPOOL, + StatusTag(active), obj.guid, desc); + }; + + for (auto const& obj : explicitly) + printSubPool(obj); + for (auto const& obj : equal) + printSubPool(obj); + } + } + + return true; + } + + static bool HandlePoolLookupCommand(ChatHandler* handler) + { + Player* player = handler->GetSession()->GetPlayer(); + if (!player) + return false; + + // Check targeted creature + Creature* target = handler->getSelectedCreature(); + if (target && target->GetSpawnId()) + { + uint32 spawnId = target->GetSpawnId(); + uint32 poolId = sPoolMgr->GetCreaturePoolId(spawnId); + if (poolId) + { + bool spawned = sPoolMgr->IsSpawnedObject(spawnId); + handler->PSendSysMessage(LANG_POOL_LOOKUP_IN_POOL, + target->GetName(), spawnId, poolId, StatusTag(spawned)); + handler->PSendSysMessage(LANG_POOL_LOOKUP_USE_INFO, poolId); + } + else + handler->PSendSysMessage(LANG_POOL_LOOKUP_NOT_IN_POOL, + target->GetName(), spawnId); + + return true; + } + + // Check nearby gameobject + GameObject* goTarget = handler->GetNearbyGameObject(); + if (goTarget && goTarget->GetSpawnId()) + { + uint32 spawnId = goTarget->GetSpawnId(); + uint32 poolId = sPoolMgr->GetGameObjectPoolId(spawnId); + if (poolId) + { + bool spawned = sPoolMgr->IsSpawnedObject(spawnId); + handler->PSendSysMessage(LANG_POOL_LOOKUP_IN_POOL, + goTarget->GetName(), spawnId, poolId, StatusTag(spawned)); + handler->PSendSysMessage(LANG_POOL_LOOKUP_USE_INFO, poolId); + } + else + handler->PSendSysMessage(LANG_POOL_LOOKUP_NOT_IN_POOL, + goTarget->GetName(), spawnId); + + return true; + } + + handler->SendSysMessage(LANG_POOL_LOOKUP_NOTARGET); + handler->SetSentErrorMessage(true); + return false; + } +}; + +void AddSC_pool_commandscript() +{ + new pool_commandscript(); +} diff --git a/src/server/scripts/Commands/cs_reload.cpp b/src/server/scripts/Commands/cs_reload.cpp index e04795f1d..ef121f039 100644 --- a/src/server/scripts/Commands/cs_reload.cpp +++ b/src/server/scripts/Commands/cs_reload.cpp @@ -150,6 +150,7 @@ public: { "skill_fishing_base_level", HandleReloadSkillFishingBaseLevelCommand, SEC_ADMINISTRATOR, Console::Yes }, { "skinning_loot_template", HandleReloadLootTemplatesSkinningCommand, SEC_ADMINISTRATOR, Console::Yes }, { "smart_scripts", HandleReloadSmartScripts, SEC_ADMINISTRATOR, Console::Yes }, + { "spawn_group", HandleReloadSpawnGroupCommand, SEC_ADMINISTRATOR, Console::Yes }, { "spell_required", HandleReloadSpellRequiredCommand, SEC_ADMINISTRATOR, Console::Yes }, { "spell_area", HandleReloadSpellAreaCommand, SEC_ADMINISTRATOR, Console::Yes }, { "spell_bonus_data", HandleReloadSpellBonusesCommand, SEC_ADMINISTRATOR, Console::Yes }, @@ -1258,6 +1259,15 @@ public: handler->SendGlobalGMSysMessage("DB table `game_graveyard` reloaded."); return true; } + + static bool HandleReloadSpawnGroupCommand(ChatHandler* handler) + { + LOG_INFO("server.loading", "Reloading spawn_group_template and spawn_group tables..."); + sObjectMgr->LoadSpawnGroupTemplates(); + sObjectMgr->LoadSpawnGroups(); + handler->SendGlobalGMSysMessage("DB tables `spawn_group_template` and `spawn_group` reloaded."); + return true; + } }; void AddSC_reload_commandscript() diff --git a/src/server/scripts/Commands/cs_script_loader.cpp b/src/server/scripts/Commands/cs_script_loader.cpp index a3e68d004..9507a4bdd 100644 --- a/src/server/scripts/Commands/cs_script_loader.cpp +++ b/src/server/scripts/Commands/cs_script_loader.cpp @@ -51,6 +51,7 @@ void AddSC_modify_commandscript(); void AddSC_npc_commandscript(); void AddSC_pet_commandscript(); void AddSC_player_commandscript(); +void AddSC_pool_commandscript(); void AddSC_pooltools_commandscript(); void AddSC_quest_commandscript(); void AddSC_reload_commandscript(); @@ -107,6 +108,7 @@ void AddCommandsScripts() AddSC_npc_commandscript(); AddSC_pet_commandscript(); AddSC_player_commandscript(); + AddSC_pool_commandscript(); AddSC_pooltools_commandscript(); AddSC_quest_commandscript(); AddSC_reload_commandscript();