feat(Core/Maps): port spawn system/dynamic spawns from TrinityCore (#25206)

Co-authored-by: r00ty-tc <r00ty-tc@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew 2026-04-12 17:52:01 -03:00 committed by GitHub
parent a8d327f21c
commit f5c4de92eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1456 additions and 114 deletions

View file

@ -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
#
###################################################################################################

View file

@ -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)

View file

@ -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
};

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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;
}

View file

@ -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;
};
};

View file

@ -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<time_t>(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<Creature>(m_spawnId) : 0;
if (poolid)
sPoolMgr->UpdatePool<Creature>(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<Creature>(m_spawnId) : 0;
if (poolid)
sPoolMgr->UpdatePool<Creature>(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);
}

View file

@ -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

View file

@ -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<HighGuid::GameObject>(), entry, map, phaseMask, x, y, z, ang, data->rotation, animprogress, go_state, artKit))
return false;

View file

@ -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

View file

@ -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<uint16>();
data.posX = fields[3].Get<float>();
@ -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<uint32>();
SpawnGroupTemplateData& group = _spawnGroupDataStore[groupId];
group.groupId = groupId;
group.name = fields[1].Get<std::string>();
group.mapId = SPAWNGROUP_MAP_UNSET;
uint32 flags = fields[2].Get<uint32>();
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<uint32>(), 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>();
uint32 type = fields[1].Get<uint8>();
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<uint32>();
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<Creature>(spawnId);
else if (spawnType == SPAWN_TYPE_GAMEOBJECT)
poolId = sPoolMgr->IsPartOfAPool<GameObject>(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<SpawnData*>(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();

View file

@ -507,6 +507,7 @@ typedef std::map<ObjectGuid, ObjectGuid> LinkedRespawnContainer;
typedef std::unordered_map<ObjectGuid::LowType, CreatureData> CreatureDataContainer;
typedef std::unordered_map<ObjectGuid::LowType, GameObjectData> GameObjectDataContainer;
typedef std::unordered_map<uint32, SpawnGroupTemplateData> SpawnGroupDataContainer;
typedef std::multimap<uint32, SpawnData const*> SpawnGroupLinkContainer;
typedef std::map<TempSummonGroupKey, std::vector<TempSummonData> > TempSummonDataContainer;
typedef std::map<TempSummonGroupKey, std::vector<GameObjectSummonData> > GameObjectSummonDataContainer;
typedef std::unordered_map<uint32, CreatureLocale> 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<SpawnGroupLinkContainer::const_iterator, SpawnGroupLinkContainer::const_iterator> 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;

View file

@ -22,6 +22,7 @@
#include "DynamicObject.h"
#include "GameObject.h"
#include "GridNotifiers.h"
#include "ObjectMgr.h"
#include "Transport.h"
template <class T>
@ -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();

View file

@ -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>();
uint32 respawnTime = fields[1].Get<uint32>();
time_t respawnTime = time_t(fields[1].Get<uint32>());
_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>();
uint32 respawnTime = fields[1].Get<uint32>();
time_t respawnTime = time_t(fields[1].Get<uint32>());
_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<WorldObject*> 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<Creature>(spawnId))
{
sPoolMgr->UpdatePool<Creature>(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<GameObject>(spawnId))
{
sPoolMgr->UpdatePool<GameObject>(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());

View file

@ -35,11 +35,13 @@
#include "PathGenerator.h"
#include "Position.h"
#include "SharedDefines.h"
#include "SpawnData.h"
#include "Timer.h"
#include "GridTerrainData.h"
#include <bitset>
#include <list>
#include <memory>
#include <set>
#include <shared_mutex>
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<ObjectGuid::LowType, time_t> const& GetCreatureRespawnTimes() const { return _creatureRespawnTimes; }
[[nodiscard]] std::unordered_map<ObjectGuid::LowType, time_t> 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<ObjectGuid::LowType /*dbGUID*/, time_t> _creatureRespawnTimes;
std::unordered_map<ObjectGuid::LowType /*dbGUID*/, time_t> _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<RespawnEntry> _respawnQueue;
std::unordered_set<uint32> _toggledSpawnGroupIds;
uint32 _respawnCheckTimer{0};
std::unordered_map<uint32, uint32> _zonePlayerCountMap;
ZoneDynamicInfoMap _zoneDynamicInfo;

View file

@ -19,6 +19,7 @@
#define AZEROTHCORE_SPAWNDATA_H
#include "Define.h"
#include "ObjectGuid.h"
#include <string>
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};

View file

@ -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

View file

@ -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<uint32>();
PoolTemplateData& pPoolTemplate = mPoolTemplate[pool_id];
pPoolTemplate.MaxLimit = fields[1].Get<uint32>();
pPoolTemplate.MaxLimit = fields[1].Get<uint32>();
pPoolTemplate.Description = fields[2].Get<std::string>();
++count;
} while (result->NextRow());
@ -1166,3 +1167,39 @@ template void PoolMgr::UpdatePool<Pool>(uint32 pool_id, uint32 db_guid_or_pool_i
template void PoolMgr::UpdatePool<GameObject>(uint32 pool_id, uint32 db_guid_or_pool_id);
template void PoolMgr::UpdatePool<Creature>(uint32 pool_id, uint32 db_guid_or_pool_id);
template void PoolMgr::UpdatePool<Quest>(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<Creature> const* PoolMgr::GetPoolCreatureGroup(uint32 poolId) const
{
auto itr = mPoolCreatureGroups.find(poolId);
return itr != mPoolCreatureGroups.end() ? &itr->second : nullptr;
}
PoolGroup<GameObject> const* PoolMgr::GetPoolGameObjectGroup(uint32 poolId) const
{
auto itr = mPoolGameobjectGroups.find(poolId);
return itr != mPoolGameobjectGroups.end() ? &itr->second : nullptr;
}
PoolGroup<Pool> 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;
}

View file

@ -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<PoolObject> const& GetExplicitlyChanced() const { return ExplicitlyChanced; }
std::vector<PoolObject> 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<Creature> const* GetPoolCreatureGroup(uint32 poolId) const;
PoolGroup<GameObject> const* GetPoolGameObjectGroup(uint32 poolId) const;
PoolGroup<Pool> 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<typename T>

View file

@ -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()

View file

@ -521,6 +521,8 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<float>(CONFIG_RESPAWN_DYNAMICRATE_GAMEOBJECT, "Respawn.DynamicRateGameObject", 1.0f);
SetConfigValue<uint32>(CONFIG_RESPAWN_DYNAMICMINIMUM_GAMEOBJECT, "Respawn.DynamicMinimumGameObject", 10);
SetConfigValue<bool>(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC, "Respawn.DynamicEscortNPC", false);
SetConfigValue<bool>(CONFIG_RESPAWN_FORCE_COMPATIBILITY_MODE, "Respawn.ForceCompatibilityMode", false);
SetConfigValue<bool>(CONFIG_VMAP_INDOOR_CHECK, "vmap.enableIndoorCheck", true);
SetConfigValue<bool>(CONFIG_VMAP_ENABLE_LOS, "vmap.enableLOS", true);

View file

@ -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,

View file

@ -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()

View file

@ -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()

View file

@ -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<Acore::RespawnDo> 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<ObjectGuid::LowType> 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<Creature>(pair.first))
continue;
creaturesToRespawn.push_back(pair.first);
}
for (ObjectGuid::LowType spawnId : creaturesToRespawn)
map->SaveCreatureRespawnTime(spawnId, now);
std::vector<ObjectGuid::LowType> 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<GameObject>(pair.first))
continue;
goesToRespawn.push_back(pair.first);
}
for (ObjectGuid::LowType spawnId : goesToRespawn)
map->SaveGORespawnTime(spawnId, now);
return true;
}

View file

@ -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()

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<PoolObject> const& explicitly,
std::vector<PoolObject> 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<Creature>(obj.guid)
: sPoolMgr->IsSpawnedObject<GameObject>(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<Pool>(poolId);
if (parentPool)
handler->PSendSysMessage(" Parent pool: {}", parentPool);
// Creature members
if (PoolGroup<Creature> const* creGroup = sPoolMgr->GetPoolCreatureGroup(poolId))
{
if (!creGroup->IsEmpty())
ListPoolMembers(handler, "Creatures",
creGroup->GetExplicitlyChanced(), creGroup->GetEqualChanced(), true);
}
// GameObject members
if (PoolGroup<GameObject> const* goGroup = sPoolMgr->GetPoolGameObjectGroup(poolId))
{
if (!goGroup->IsEmpty())
ListPoolMembers(handler, "GameObjects",
goGroup->GetExplicitlyChanced(), goGroup->GetEqualChanced(), false);
}
// Sub-pool members
if (PoolGroup<Pool> 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<Pool>(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<Creature>(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<GameObject>(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();
}

View file

@ -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()

View file

@ -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();