Skip to content

feat(Core/Maps): port object life cycle system/dynamic spawns from TrinityCore#25206

Open
Nyeriah wants to merge 7 commits intoazerothcore:masterfrom
Nyeriah:dynspawns
Open

feat(Core/Maps): port object life cycle system/dynamic spawns from TrinityCore#25206
Nyeriah wants to merge 7 commits intoazerothcore:masterfrom
Nyeriah:dynspawns

Conversation

@Nyeriah
Copy link
Copy Markdown
Member

@Nyeriah Nyeriah commented Mar 24, 2026

Changes Proposed:

This PR proposes changes to:

  • Core (units, players, creatures, game systems).
  • Scripts (bosses, spell scripts, creature scripts).
  • Database (SAI, creatures, etc).

Ports TrinityCore's spawn group system to AzerothCore, enabling logical grouping of creature and gameobject spawns with per-group control over respawn behavior.

Key changes:

  • New tables: spawn_group_template (group definitions with flags) and spawn_group (creature/GO membership)
  • ProcessRespawns(): Non-compatibility mode respawn scheduler in Map::Update() — creatures are fully removed on death and recreated fresh by the scheduler when their timer expires
  • Compatibility mode: All existing spawns default to group 0 with SPAWNGROUP_FLAG_COMPATIBILITY_MODE, preserving current behavior. A Respawn.ForceCompatibilityMode worldserver config allows forcing all spawns to use legacy behavior regardless of group flags
  • GM commands: .list respawn (show pending respawns on current map), .respawn all (force-respawn everything on the map), .npc spawngroup / .go spawngroup (show spawn group info)
  • SAI integration: SMART_ACTION_SPAWN_SPAWNGROUP and SMART_ACTION_DESPAWN_SPAWNGROUP for script-driven spawn group control
  • Grid loader awareness: Skips non-compat spawns with pending respawn times during grid loading
  • Reload command: .reload spawn_group to hot-reload group data

AI-assisted Pull Requests

Important

While the use of AI tools when preparing pull requests is not prohibited, contributors must clearly disclose when such tools have been used and specify the model involved.

  • AI tools (e.g. ChatGPT, Claude, or similar) were used entirely or partially in preparing this pull request. Claude Code (Claude Opus 4.6) was used to assist with porting, adapting, and debugging the implementation.

Issues Addressed:

  • Closes N/A — this is a new feature port

SOURCE:

The changes have been validated through:

  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick). Cherry-picks must be committed using the proper --author tag in order to be accepted, thus crediting the original authors, unless otherwise unable to be found

Based on TrinityCore commit 59db2eee by r00ty-tc, adapted for AzerothCore's codebase (different DB access patterns, LoadCreatureFromDB signatures, existing dynamic respawn system, config framework, etc.)

Tests Performed:

This PR has been:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author/has been live on production servers.
  • This pull request requires further testing and may have edge cases to be tested.

How to Test the Changes:

  • This pull request requires further testing. Provide steps to test your changes.
  1. Apply the SQL updates and rebuild the worldserver
  2. Set Respawn.ForceCompatibilityMode = 0 in worldserver.conf to enable the new system
  3. Kill a creature in the open world and observe: the corpse should decay, the creature should be fully removed, then respawn fresh after the respawn timer
  4. Use .list respawn to verify creatures appear in the pending respawn list with correct timers
  5. Use .respawn all to force immediate respawn of all pending creatures/GOs on the map
  6. Verify .npc spawngroup and .go spawngroup show correct group info for targeted spawns
  7. Set Respawn.ForceCompatibilityMode = 1 and verify legacy corpse-stays-on-map behavior is restored

Known Issues and TODO List:

  • Spawn group data tables (spawn_group_template, spawn_group) ship with only default groups — individual creature/GO assignments to custom groups need to be populated by future DB PRs
  • Instance/boss spawn group integration (e.g. toggling boss-related spawn groups on encounter state changes) is scaffolded but not yet wired to specific encounters
  • Edge cases with phasing and cross-grid respawns may need further testing

🤖 Generated with Claude Code

Nyeriah and others added 3 commits March 24, 2026 19:00
Adds spawn_group_template and spawn_group tables for managing creature
and gameobject spawns in logical groups. Supports per-group flags for
compatibility mode, dynamic spawn control, and system management.

Includes ProcessRespawns() for non-compatibility mode respawn scheduling,
.list respawn / .respawn all GM commands, SAI spawn group actions, and
Respawn.ForceCompatibilityMode worldserver config option.

Based on TrinityCore commit 59db2eeea0.

Co-Authored-By: r00ty-tc <r00ty-tc@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AC's LoadFromDB() wrapper passes addToMap=false, so ProcessRespawns()
was creating creatures/GOs but never adding them to the world. Use
LoadCreatureFromDB/LoadGameObjectFromDB directly with addToMap=true.

Also fix RemoveCorpse always saving respawn time in non-compat mode
and update .list respawn output format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 24, 2026 22:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports TrinityCore’s spawn group system into AzerothCore to allow grouping spawns (creatures/gameobjects) with per-group activation and respawn behavior, including a non-compatibility respawn scheduler and admin/SAI controls.

Changes:

  • Added spawn group template/member loading in ObjectMgr, plus map-level spawn/despawn toggling and a periodic Map::ProcessRespawns() scheduler.
  • Added GM/admin commands for listing pending respawns, forcing respawns, reloading spawn group tables, and spawning/despawning groups.
  • Added config toggles for forcing legacy respawn behavior and (documented) escort NPC dynamic respawn behavior, plus initial SQL tables.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/server/scripts/Commands/cs_reload.cpp Adds .reload spawn_group command to hot-reload spawn group data
src/server/scripts/Commands/cs_npc.cpp Adds .npc spawngroup / .npc despawngroup commands
src/server/scripts/Commands/cs_misc.cpp Extends respawn-all logic to also clear non-compat pending respawns
src/server/scripts/Commands/cs_list.cpp Adds .list respawns to show pending creature/GO respawn timers
src/server/scripts/Commands/cs_gobject.cpp Adds .go spawngroup / .go despawngroup commands
src/server/game/World/WorldConfig.h Adds new config keys for spawn/respawn behavior
src/server/game/World/WorldConfig.cpp Registers new config values
src/server/game/World/World.cpp Loads spawn group templates and members during server startup
src/server/game/Maps/SpawnData.h Adds compatibility flag, per-spawn spawnId, and mapId changes for group templates
src/server/game/Maps/Map.h Adds spawn group APIs, respawn processing, and respawn-time accessors
src/server/game/Maps/Map.cpp Implements group activation, spawn/despawn, and periodic respawn processing
src/server/game/Grids/GridObjectLoader.cpp Skips loading spawns whose spawn group is inactive
src/server/game/Globals/ObjectMgr.h Adds spawn group load APIs and group-to-spawn lookup storage
src/server/game/Globals/ObjectMgr.cpp Implements spawn group template/member DB loading and bookkeeping
src/server/game/Entities/GameObject/GameObject.h Tracks per-GO compatibility mode flag
src/server/game/Entities/GameObject/GameObject.cpp Sets GO compatibility mode from spawn group flags/config
src/server/game/Entities/Creature/Creature.h Tracks per-creature compatibility mode flag
src/server/game/Entities/Creature/Creature.cpp Changes corpse removal/respawn behavior for non-compat mode and sets compatibility mode on load
src/server/game/AI/SmartScripts/SmartScriptMgr.h Adds params struct for spawn group SAI actions
src/server/game/AI/SmartScripts/SmartScriptMgr.cpp Enables SAI validation/param sizing for spawn group actions
src/server/game/AI/SmartScripts/SmartScript.cpp Implements SAI actions to spawn/despawn spawn groups
src/server/game/AI/ScriptedAI/ScriptedEscortAI.h Marks escort AI as escort NPC via new virtual
src/server/game/AI/CreatureAI.h Adds virtual IsEscortNPC() hook
src/server/database/Database/Implementation/WorldDatabase.h Adds prepared statement id for deleting spawn group members
src/server/database/Database/Implementation/WorldDatabase.cpp Prepares DELETE FROM spawn_group ... statement
src/server/apps/worldserver/worldserver.conf.dist Documents/configures new respawn toggles
data/sql/updates/pending_db_world/rev_spawn_group_tables.sql Adds initial spawn_group_template and spawn_group tables with defaults
Comments suppressed due to low confidence (1)

src/server/scripts/Commands/cs_misc.cpp:1

  • PR description says .respawn all 'force-respawn everything on the map', but this implementation only clears respawn times for the player's current grid ID (and the phase-1 visit is limited to activation range). If the intended behavior is truly 'entire map', remove the grid filter and iterate all respawn times / loaded grids accordingly; otherwise, clarify the command behavior in the PR description/help text.
/*

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2588 to +2596
if (!creature->LoadFromDB(spawnId, this, true))
delete creature;
break;
}
case SPAWN_TYPE_GAMEOBJECT:
{
GameObject* gameobject = new GameObject();
if (!gameobject->LoadFromDB(spawnId, this))
delete gameobject;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creature::LoadFromDB / GameObject::LoadFromDB wrappers in this codebase pass addToMap = false (see the wrapper definitions), but SpawnGroupSpawn() never performs a follow-up add-to-map step. If LoadFromDB() succeeds, these objects can leak and/or never appear in-world. Use the same add-to-map loading paths used by ProcessRespawns() (e.g., LoadCreatureFromDB(..., true, ...) / LoadGameObjectFromDB(..., true)) or explicitly add the created object to the map on successful load.

Suggested change
if (!creature->LoadFromDB(spawnId, this, true))
delete creature;
break;
}
case SPAWN_TYPE_GAMEOBJECT:
{
GameObject* gameobject = new GameObject();
if (!gameobject->LoadFromDB(spawnId, this))
delete gameobject;
if (!creature->LoadFromDB(spawnId, this, true) || !AddToMap(creature))
{
delete creature;
}
break;
}
case SPAWN_TYPE_GAMEOBJECT:
{
GameObject* gameobject = new GameObject();
if (!gameobject->LoadFromDB(spawnId, this) || !AddToMap(gameobject))
{
delete gameobject;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — switched to LoadCreatureFromDB/LoadGameObjectFromDB with addToMap=true in both SpawnGroupSpawn and ProcessRespawns.

void ObjectMgr::LoadSpawnGroupTemplates()
{
uint32 oldMSTime = getMSTime();

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadSpawnGroupTemplates() populates/overwrites _spawnGroupDataStore but does not clear existing entries first. After .reload spawn_group, groups deleted from DB will remain in memory and can still be referenced by ID. For hot-reload correctness, clear/rebuild the in-memory template cache (while re-establishing required system defaults like group 0/1) before loading from DB.

Suggested change
// Clear existing templates to avoid keeping stale groups across reloads
_spawnGroupDataStore.clear();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — _spawnGroupDataStore is now cleared before loading.

void ObjectMgr::LoadSpawnGroups()
{
uint32 oldMSTime = getMSTime();

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadSpawnGroups() does not reset prior state on reload: _spawnGroupMapStore is never cleared, and existing SpawnData::spawnGroupId assignments are never reset back to default before re-applying DB membership. This will leave stale memberships if rows are removed/changed in DB and can also duplicate entries in _spawnGroupMapStore across reloads. Recommended fix: clear _spawnGroupMapStore, reset spawnGroupId on all spawns back to the default group, then rebuild memberships from spawn_group.

Suggested change
// Clear previous spawn group state before reloading from the database
_spawnGroupMapStore.clear();
// Reset spawnGroupId for all spawns back to the default group (0)
for (auto& creaturePair : _creatureDataStore)
creaturePair.second.spawnGroupId = 0;
for (auto& gameObjectPair : _gameObjectDataStore)
gameObjectPair.second.spawnGroupId = 0;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — _spawnGroupMapStore is cleared and all spawnGroupId fields are reset to 0 before reloading.

Comment on lines +8840 to +8842
const_cast<SpawnData*>(data)->spawnGroupId = groupId;
if (!(groupTemplate.flags & SPAWNGROUP_FLAG_SYSTEM))
_spawnGroupMapStore.emplace(groupId, data);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadSpawnGroups() does not reset prior state on reload: _spawnGroupMapStore is never cleared, and existing SpawnData::spawnGroupId assignments are never reset back to default before re-applying DB membership. This will leave stale memberships if rows are removed/changed in DB and can also duplicate entries in _spawnGroupMapStore across reloads. Recommended fix: clear _spawnGroupMapStore, reset spawnGroupId on all spawns back to the default group, then rebuild memberships from spawn_group.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above — addressed in the same fix.

Comment on lines +2 to +3
DROP TABLE IF EXISTS `spawn_group_template`;
CREATE TABLE `spawn_group_template` (
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the tables in an update script is destructive and will delete existing spawn group data for servers that may already have created/populated these tables (e.g., during testing or incremental deployments). Prefer non-destructive migrations: use CREATE TABLE IF NOT EXISTS (or guarded create), and use INSERT ... ON DUPLICATE KEY UPDATE / REPLACE INTO for the default rows, rather than DROP TABLE.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are pending update files — they run once during DB setup. DROP TABLE IF EXISTS is the standard pattern for initial table creation in AC pending SQL files. Added a DELETE before the INSERT to satisfy the SQL codestyle check.

Comment on lines +203 to +204
{ "spawngroup", HandleNpcSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No },
{ "despawngroup", HandleNpcDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No }
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says .npc spawngroup / .go spawngroup 'show spawn group info for targeted spawns', but these handlers take a groupId and perform spawn/despawn operations. Update the description (and in-game help if any) to reflect that these commands control group activation, or consider adding separate informational commands if the intent is to show target info.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — these are spawn/despawn control commands, not informational. PR description will be clarified.

Comment on lines +2666 to +2668
void Map::ProcessRespawns()
{
time_t now = GameTime::GetGameTime().count();
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessRespawns() does a full scan of _creatureRespawnTimes and _goRespawnTimes every 5 seconds per map. On busy servers with many pending respawns, this becomes O(N) periodic work per map and can scale poorly. Consider tracking the next due respawn time (or using a min-heap/priority queue keyed by respawn timestamp) so the update loop only processes due entries and sleeps until the next due time.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches TrinityCore's implementation. In practice the number of pending respawns on a single map is small (creatures die and respawn within seconds to minutes). A priority queue optimization could be done as a follow-up if profiling shows it's needed.

Comment on lines +2672 to +2676
for (auto const& pair : _creatureRespawnTimes)
{
if (pair.second <= now)
toRespawnCreatures.push_back(pair.first);
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessRespawns() does a full scan of _creatureRespawnTimes and _goRespawnTimes every 5 seconds per map. On busy servers with many pending respawns, this becomes O(N) periodic work per map and can scale poorly. Consider tracking the next due respawn time (or using a min-heap/priority queue keyed by respawn timestamp) so the update loop only processes due entries and sleeps until the next due time.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

Comment on lines +42 to +45
// Skip creatures in manual-spawn groups unless the group is active on this map
CreatureData const* cData = sObjectMgr->GetCreatureData(guid);
if (cData && !map->IsSpawnGroupActive(cData->spawnGroupId))
continue;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this only applies to 'manual-spawn groups', but the condition skips any spawn whose group is inactive (including non-manual groups that were toggled off). Update the comment to reflect the actual behavior (e.g., 'Skip spawns whose spawn group is not active on this map').

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the comments to accurately reflect the behavior.

Comment on lines +427 to +428
[[nodiscard]] std::unordered_map<ObjectGuid::LowType, time_t> const& GetCreatureRespawnTimes() const { return _creatureRespawnTimes; }
[[nodiscard]] std::unordered_map<ObjectGuid::LowType, time_t> const& GetGORespawnTimes() const { return _goRespawnTimes; }
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These getters expose the concrete internal container type (std::unordered_map) as part of Map's interface, which tightly couples call-sites (scripts/commands) to the implementation and makes future refactors harder. Consider returning an abstracted view (e.g., iterator range, callback-based enumeration, or a lightweight span-like wrapper) or adding dedicated query APIs for the command use-cases (count/iterate with limit/filtering).

Suggested change
[[nodiscard]] std::unordered_map<ObjectGuid::LowType, time_t> const& GetCreatureRespawnTimes() const { return _creatureRespawnTimes; }
[[nodiscard]] std::unordered_map<ObjectGuid::LowType, time_t> const& GetGORespawnTimes() const { return _goRespawnTimes; }
using RespawnTimesMap = std::unordered_map<ObjectGuid::LowType, time_t>;
[[nodiscard]] RespawnTimesMap const& GetCreatureRespawnTimes() const { return _creatureRespawnTimes; }
[[nodiscard]] RespawnTimesMap const& GetGORespawnTimes() const { return _goRespawnTimes; }

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the existing AC pattern — other similar getters in Map.h also expose concrete container types (e.g. GetCreatureBySpawnIdStore). Abstracting this would be over-engineering for the current use case.

@TheSCREWEDSoftware
Copy link
Copy Markdown
Contributor

Would close this #23824 ? or TC implementation is not the same as cmangos?

Currently this issue would be by this implemantion (not the pr itself) if the above is the same for TC and Cmangos

#22095 (comment)

@Nyeriah Nyeriah changed the title feat(Core/Maps): port spawn group system from TrinityCore feat(Core/Maps): port object life cycle system/dynamic spawns from TrinityCore Mar 24, 2026
@Nyeriah
Copy link
Copy Markdown
Member Author

Nyeriah commented Mar 24, 2026

Would close this #23824 ? or TC implementation is not the same as cmangos?

Currently this issue would be by this implemantion (not the pr itself) if the above is the same for TC and Cmangos

#22095 (comment)

No, completely different stuff

@github-actions github-actions bot added DB related to the SQL database CORE Related to the core Script Refers to C++ Scripts for the Core file-cpp Used to trigger the matrix build labels Mar 24, 2026
Nyeriah and others added 2 commits March 24, 2026 21:22
- Fix SpawnGroupSpawn using LoadFromDB (addToMap=false) instead of
  LoadCreatureFromDB/LoadGameObjectFromDB with addToMap=true
- Clear stale state in LoadSpawnGroupTemplates/LoadSpawnGroups for
  correct hot-reload behavior
- Add DELETE before INSERT in SQL to satisfy codestyle CI check
- Fix misleading comments in GridObjectLoader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-compat Creature::Respawn() was removing creatures from the world
and clearing their respawn time entries, so ProcessRespawns() could
never find and recreate them.

- Skip alive creatures in non-compat path (nothing to respawn)
- Set respawn time to now instead of clearing it, so ProcessRespawns()
  picks up the spawn on the next update tick
- Fix .respawn all Phase 2 to also set time to now instead of erasing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…roup commands

Register list respawns, npc/gobject spawngroup/despawngroup, and
reload spawn_group in the command table. Move all hardcoded output
strings to acore_string with LANG_ constants and translations for
all 9 supported locales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Rorschach91 Rorschach91 added the Requires WIKI Update Wiki sources will need to be updated after merging this PR. label Mar 26, 2026
# Conflicts:
#	src/server/game/Miscellaneous/Language.h
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE Related to the core DB related to the SQL database file-cpp Used to trigger the matrix build Ready to be Reviewed Requires WIKI Update Wiki sources will need to be updated after merging this PR. Script Refers to C++ Scripts for the Core Waiting to be Tested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants