Merge branch 'master' into test-staging

This commit is contained in:
Keleborn
2026-05-15 22:52:49 -07:00
32 changed files with 604 additions and 90 deletions
@@ -621,6 +621,15 @@ Allow.IP.Based.Action.Logging = 0
LogSpamReports = 1
#
# ChatLog.Enable
# Description: Enable or disable chat logging (chat_log.cpp).
# Default: 0 - (Disabled)
# 1 - (Enabled)
#
ChatLog.Enable = 0
#
# Appender config values: Given an appender "name"
# Appender.name
@@ -2608,6 +2617,14 @@ DurabilityLoss.InPvP = 0
DurabilityLoss.OnDeath = 10
#
# DurabilityLoss.OnSpiritResurrect
# Description: Durability loss percentage on spirit healer resurrection. Does not effect PvP
# spirit healers.
# Default: 25
DurabilityLoss.OnSpiritResurrect = 25
#
# DurabilityLossChance.Damage
# Description: Chance to lose durability on one equipped item from damage.
@@ -403,10 +403,11 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_UPD_CHARACTER_POSITION, "UPDATE characters SET position_x = ?, position_y = ?, position_z = ?, orientation = ?, map = ?, zone = ?, trans_x = 0, trans_y = 0, trans_z = 0, transguid = 0, taxi_path = '', cinematic = 1 WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_AURA_FROZEN, "SELECT characters.name FROM characters LEFT JOIN character_aura ON (characters.guid = character_aura.guid) WHERE character_aura.spell = 9454", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_ONLINE, "SELECT name, account, map, zone FROM characters WHERE online > 0", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_GUID, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL AND guid = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_NAME, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL AND deleteInfos_Name LIKE CONCAT('%%', ?, '%%')", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID, "SELECT guid, class, race FROM characters WHERE account = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_GUID, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL AND guid = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_NAME, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL AND deleteInfos_Name LIKE CONCAT('%%', ?, '%%') ORDER BY deleteDate DESC", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_NAME_LIMIT, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL AND deleteInfos_Name LIKE CONCAT('%%', ?, '%%') ORDER BY deleteDate DESC LIMIT 51", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_DEL_INFO, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL ORDER BY deleteDate DESC LIMIT 51", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID, "SELECT guid FROM characters WHERE account = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_PINFO, "SELECT totaltime, level, money, account, race, class, map, zone, gender, health, playerFlags FROM characters WHERE guid = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_PINFO_BANS, "SELECT unbandate, bandate = unbandate, bannedby, banreason FROM character_banned WHERE guid = ? AND active ORDER BY bandate ASC LIMIT 1", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_PINFO_MAILS, "SELECT SUM(CASE WHEN (checked & 1) THEN 1 ELSE 0 END) AS 'readmail', COUNT(*) AS 'totalmail' FROM mail WHERE `receiver` = ?", CONNECTION_SYNCH);
@@ -333,6 +333,7 @@ enum CharacterDatabaseStatements : uint32
CHAR_SEL_CHAR_DEL_INFO_BY_GUID,
CHAR_SEL_CHAR_DEL_INFO_BY_NAME,
CHAR_SEL_CHAR_DEL_INFO_BY_NAME_LIMIT,
CHAR_SEL_CHAR_DEL_INFO,
CHAR_SEL_CHARS_BY_ACCOUNT_ID,
+4 -1
View File
@@ -300,7 +300,6 @@ enum RBACPermissions
RBAC_PERM_COMMAND_GMNOTIFY = 468,
RBAC_PERM_COMMAND_NAMEANNOUNCE = 469,
RBAC_PERM_COMMAND_NOTIFY = 470,
RBAC_PERM_COMMAND_WHISPERS = 471,
RBAC_PERM_COMMAND_GROUP = 472,
RBAC_PERM_COMMAND_GROUP_LEADER = 473,
RBAC_PERM_COMMAND_GROUP_DISBAND = 474,
@@ -678,6 +677,10 @@ enum RBACPermissions
RBAC_PERM_COMMAND_BF_QUEUE = 913,
RBAC_PERM_COMMAND_PET_LIST = 914,
RBAC_PERM_COMMAND_PET_DELETE = 915,
RBAC_PERM_COMMAND_RESPAWN_CREATURE_GUID = 916,
RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_GUID = 917,
RBAC_PERM_COMMAND_RESPAWN_CREATURE_ENTRY = 918,
RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY = 919,
// custom permissions 1000+
RBAC_PERM_MAX
};
+3 -1
View File
@@ -262,7 +262,9 @@ void WorldSession::SendSpiritResurrect()
{
_player->ResurrectPlayer(0.5f, true);
_player->DurabilityLossAll(0.25f, true);
float durabilityLossOnSpiritResurrect = sWorld->getRate(RATE_DURABILITY_LOSS_ON_SPIRIT_RESURRECT) / 100.0f;
if (durabilityLossOnSpiritResurrect)
_player->DurabilityLossAll(durabilityLossOnSpiritResurrect, true);
// get corpse nearest graveyard
GraveyardStruct const* corpseGrave = nullptr;
+20 -10
View File
@@ -2724,12 +2724,18 @@ void Map::ProcessRespawns()
void Map::ProcessCreatureRespawn(ObjectGuid::LowType spawnId)
{
// Pool members are handled entirely by PoolMgr
if (uint32 poolId = sPoolMgr->IsPartOfAPool<Creature>(spawnId))
// Pool members in non-instanced maps are handled entirely by PoolMgr.
// In instanced maps the pool system operates globally and Spawn1Object is
// a no-op for instanceable maps, so fall through to the normal per-instance
// respawn logic instead.
if (!Instanceable())
{
sPoolMgr->UpdatePool<Creature>(poolId, spawnId);
RemoveCreatureRespawnTime(spawnId);
return;
if (uint32 poolId = sPoolMgr->IsPartOfAPool<Creature>(spawnId))
{
sPoolMgr->UpdatePool<Creature>(poolId, spawnId);
RemoveCreatureRespawnTime(spawnId);
return;
}
}
CreatureData const* data = sObjectMgr->GetCreatureData(spawnId);
@@ -2785,12 +2791,16 @@ void Map::ProcessCreatureRespawn(ObjectGuid::LowType spawnId)
void Map::ProcessGameObjectRespawn(ObjectGuid::LowType spawnId)
{
// Pool members are handled entirely by PoolMgr
if (uint32 poolId = sPoolMgr->IsPartOfAPool<GameObject>(spawnId))
// Same rationale as ProcessCreatureRespawn: pool management via PoolMgr is
// only meaningful for non-instanced maps where Spawn1Object actually spawns.
if (!Instanceable())
{
sPoolMgr->UpdatePool<GameObject>(poolId, spawnId);
RemoveGORespawnTime(spawnId);
return;
if (uint32 poolId = sPoolMgr->IsPartOfAPool<GameObject>(spawnId))
{
sPoolMgr->UpdatePool<GameObject>(poolId, spawnId);
RemoveGORespawnTime(spawnId);
return;
}
}
GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId);
+22 -1
View File
@@ -1487,6 +1487,27 @@ enum AcoreStrings
// Pet delete command
LANG_PET_DELETE_NOT_FOUND = 35438,
LANG_PET_DELETE_SUCCESS = 35439
LANG_PET_DELETE_SUCCESS = 35439,
LANG_CHARACTER_DELETED_LIST_LIMIT = 35440,
// Respawn creature/gameobject by spawn GUID commands
LANG_RESPAWN_GUID_CREATURE_NOT_FOUND = 35441,
LANG_RESPAWN_GUID_MAP_NOT_LOADED = 35442,
LANG_RESPAWN_GUID_CREATURE_ALIVE = 35443,
LANG_RESPAWN_GUID_CREATURE_QUEUED = 35444,
LANG_RESPAWN_GUID_GAMEOBJECT_ACTIVE = 35445,
LANG_RESPAWN_GUID_GAMEOBJECT_QUEUED = 35446,
// 35447 is reserved for LANG_LIST_RESPAWNS_NO_MAP (list respawns command, defined at the end of this enum)
LANG_RESPAWN_GUID_GAMEOBJECT_NOT_FOUND = 35448,
// Respawn creature/gameobject by entry commands
LANG_RESPAWN_ENTRY_CREATURE_NOT_FOUND = 35449,
LANG_RESPAWN_ENTRY_GAMEOBJECT_NOT_FOUND = 35450,
LANG_RESPAWN_ENTRY_CREATURE_QUEUED = 35451,
LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED = 35452,
// List respawns console support
LANG_LIST_RESPAWNS_NO_MAP = 35447
};
#endif
+4
View File
@@ -138,6 +138,8 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<float>(RATE_DURABILITY_LOSS_ON_DEATH, "DurabilityLoss.OnDeath", 10.0f, ConfigValueCache::Reloadable::Yes, [](float const& value) { return value >= 0.0f && value <= 100.0f; }, ">= 0 && <= 100");
SetConfigValue<float>(RATE_DURABILITY_LOSS_ON_SPIRIT_RESURRECT, "DurabilityLoss.OnSpiritResurrect", 25.0f, ConfigValueCache::Reloadable::Yes, [](float const& value) { return value >= 0.0f && value <= 100.0f; }, ">= 0 && <= 100");
SetConfigValue<float>(RATE_DURABILITY_LOSS_DAMAGE, "DurabilityLossChance.Damage", 0.5f, ConfigValueCache::Reloadable::Yes, [](float const& value) { return value >= 0.0f; }, ">= 0");
SetConfigValue<float>(RATE_DURABILITY_LOSS_ABSORB, "DurabilityLossChance.Absorb", 0.5f, ConfigValueCache::Reloadable::Yes, [](float const& value) { return value >= 0.0f; }, ">= 0");
SetConfigValue<float>(RATE_DURABILITY_LOSS_PARRY, "DurabilityLossChance.Parry", 0.05f, ConfigValueCache::Reloadable::Yes, [](float const& value) { return value >= 0.0f; }, ">= 0");
@@ -602,6 +604,8 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<bool>(CONFIG_LOGSPAMREPORTS, "LogSpamReports", true);
SetConfigValue<bool>(CONFIG_CHATLOG_ENABLED, "ChatLog.Enable", false);
// Whether to use LoS from game objects
SetConfigValue<bool>(CONFIG_CHECK_GOBJECT_LOS, "CheckGameObjectLoS", true);
+2
View File
@@ -480,6 +480,7 @@ enum ServerConfigs
RATE_CORPSE_DECAY_LOOTED,
RATE_INSTANCE_RESET_TIME,
RATE_DURABILITY_LOSS_ON_DEATH,
RATE_DURABILITY_LOSS_ON_SPIRIT_RESURRECT,
RATE_DURABILITY_LOSS_DAMAGE,
RATE_DURABILITY_LOSS_PARRY,
RATE_DURABILITY_LOSS_ABSORB,
@@ -491,6 +492,7 @@ enum ServerConfigs
CONFIG_NEW_CHAR_STRING,
CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS,
CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW,
CONFIG_CHATLOG_ENABLED,
MAX_NUM_SERVER_CONFIGS
};
+21 -6
View File
@@ -95,18 +95,22 @@ public:
uint32 accountId; ///< the account id
std::string accountName; ///< the account name
time_t deleteDate; ///< the date at which the character has been deleted
uint8 level; ///< the character level at the time of deletion
};
typedef std::list<DeletedInfo> DeletedInfoList;
static constexpr std::size_t MAX_DELETED_CHAR_RESULTS = 50;
/**
* Collects all GUIDs (and related info) from deleted characters which are still in the database.
*
* @param foundList a reference to an std::list which will be filled with info data
* @param searchString the search string which either contains a player GUID or a part fo the character-name
* @param searchString the search string which either contains a player GUID or a part of the character-name
* @param limitResults if true, caps results at MAX_DELETED_CHAR_RESULTS + 1 using a DB-level LIMIT
* @return returns false if there was a problem while selecting the characters (e.g. player name not normalizeable)
*/
static bool GetDeletedCharacterInfoList(DeletedInfoList& foundList, std::string searchString)
static bool GetDeletedCharacterInfoList(DeletedInfoList& foundList, std::string searchString, bool limitResults = false)
{
PreparedQueryResult result;
CharacterDatabasePreparedStatement* stmt = nullptr;
@@ -125,7 +129,8 @@ public:
if (!normalizePlayerName(searchString))
return false;
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_DEL_INFO_BY_NAME);
CharacterDatabaseStatements nameStmt = limitResults ? CHAR_SEL_CHAR_DEL_INFO_BY_NAME_LIMIT : CHAR_SEL_CHAR_DEL_INFO_BY_NAME;
stmt = CharacterDatabase.GetPreparedStatement(nameStmt);
stmt->SetData(0, searchString);
result = CharacterDatabase.Query(stmt);
}
@@ -151,6 +156,7 @@ public:
// account name will be empty for nonexisting account
AccountMgr::GetName(info.accountId, info.accountName);
info.deleteDate = time_t(fields[3].Get<uint32>());
info.level = fields[4].Get<uint8>();
foundList.push_back(info);
} while (result->NextRow());
}
@@ -183,11 +189,13 @@ public:
if (!handler->GetSession())
handler->PSendSysMessage(LANG_CHARACTER_DELETED_LIST_LINE_CONSOLE,
itr->lowGuid, itr->name, itr->accountName.empty() ? "<Not existing>" : itr->accountName,
itr->lowGuid, itr->name, uint32(itr->level),
itr->accountName.empty() ? "<Not existing>" : itr->accountName,
itr->accountId, dateStr);
else
handler->PSendSysMessage(LANG_CHARACTER_DELETED_LIST_LINE_CHAT,
itr->lowGuid, itr->name, itr->accountName.empty() ? "<Not existing>" : itr->accountName,
itr->lowGuid, itr->name, uint32(itr->level),
itr->accountName.empty() ? "<Not existing>" : itr->accountName,
itr->accountId, dateStr);
}
@@ -590,7 +598,7 @@ public:
if (needleStr)
needle.assign(*needleStr);
DeletedInfoList foundList;
if (!GetDeletedCharacterInfoList(foundList, needle))
if (!GetDeletedCharacterInfoList(foundList, needle, true))
return false;
// if no characters have been found, output a warning
@@ -600,8 +608,15 @@ public:
return false;
}
bool truncated = foundList.size() > MAX_DELETED_CHAR_RESULTS;
if (truncated)
foundList.resize(MAX_DELETED_CHAR_RESULTS);
HandleCharacterDeletedListHelper(foundList, handler);
if (truncated)
handler->SendSysMessage(LANG_CHARACTER_DELETED_LIST_LIMIT);
return true;
}
+30 -8
View File
@@ -51,7 +51,7 @@ public:
{ "item", HandleListItemCommand, rbac::RBAC_PERM_COMMAND_LIST_ITEM, Console::Yes },
{ "object", HandleListObjectCommand, rbac::RBAC_PERM_COMMAND_LIST_OBJECT, Console::Yes },
{ "auras", listAurasCommandTable },
{ "respawns", HandleListRespawnsCommand, rbac::RBAC_PERM_COMMAND_LIST_CREATURE, Console::No },
{ "respawns", HandleListRespawnsCommand, rbac::RBAC_PERM_COMMAND_LIST_RESPAWNS, Console::Yes },
};
static ChatCommandTable commandTable =
{
@@ -521,13 +521,35 @@ public:
return true;
}
static bool HandleListRespawnsCommand(ChatHandler* handler)
static bool HandleListRespawnsCommand(ChatHandler* handler, Optional<uint32> firstArg, Optional<uint32> secondArg, Optional<uint32> thirdArg)
{
Player* player = handler->GetSession()->GetPlayer();
if (!player)
return false;
Map* map = nullptr;
Optional<uint32> entryFilter;
if (handler->GetSession())
{
// In-game: first arg = entryId (optional), use player's current map
map = handler->GetSession()->GetPlayer()->GetMap();
entryFilter = firstArg;
}
else
{
// Console: first arg = mapId (required), second = instanceId, third = entryId
if (!firstArg)
{
handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP);
return false;
}
map = sMapMgr->FindMap(*firstArg, secondArg.value_or(0));
entryFilter = thirdArg;
}
if (!map)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, firstArg.value_or(0));
return false;
}
Map* map = player->GetMap();
uint32 count = 0;
time_t now = GameTime::GetGameTime().count();
@@ -535,7 +557,7 @@ public:
for (auto const& pair : map->GetCreatureRespawnTimes())
{
CreatureData const* data = sObjectMgr->GetCreatureData(pair.first);
if (!data)
if (!data || (entryFilter && data->id1 != *entryFilter))
continue;
CreatureTemplate const* cTemplate = sObjectMgr->GetCreatureTemplate(data->id1);
@@ -555,7 +577,7 @@ public:
for (auto const& pair : map->GetGORespawnTimes())
{
GameObjectData const* data = sObjectMgr->GetGameObjectData(pair.first);
if (!data)
if (!data || (entryFilter && data->id != *entryFilter))
continue;
GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(data->id);
+1 -1
View File
@@ -45,7 +45,7 @@ public:
{ "gmannounce", HandleGMAnnounceCommand, rbac::RBAC_PERM_COMMAND_GMANNOUNCE, Console::Yes },
{ "notify", HandleNotifyCommand, rbac::RBAC_PERM_COMMAND_NOTIFY, Console::Yes },
{ "gmnotify", HandleGMNotifyCommand, rbac::RBAC_PERM_COMMAND_GMNOTIFY, Console::Yes },
{ "whispers", HandleWhispersCommand, rbac::RBAC_PERM_COMMAND_WHISPERS, Console::No },
{ "whispers", HandleWhispersCommand, rbac::RBAC_PERM_CAN_FILTER_WHISPERS, Console::No },
};
return commandTable;
}
+264
View File
@@ -24,6 +24,7 @@
#include "CommandScript.h"
#include "Common.h"
#include "GameGraveyard.h"
#include "GameObject.h"
#include "GameTime.h"
#include "GridNotifiers.h"
#include "GridTerrainLoader.h"
@@ -177,6 +178,10 @@ public:
{ "pinfo", HandlePInfoCommand, rbac::RBAC_PERM_COMMAND_PINFO, Console::Yes },
{ "respawn", HandleRespawnCommand, rbac::RBAC_PERM_COMMAND_RESPAWN, Console::No },
{ "respawn all", HandleRespawnAllCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_ALL, Console::No },
{ "respawn creature guid", HandleRespawnCreatureByGuidCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_CREATURE_GUID, Console::Yes },
{ "respawn gameobject guid", HandleRespawnGameObjectByGuidCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_GUID, Console::Yes },
{ "respawn creature entry", HandleRespawnCreatureByEntryCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_CREATURE_ENTRY, Console::Yes },
{ "respawn gameobject entry", HandleRespawnGameObjectByEntryCommand, rbac::RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY, Console::Yes },
{ "mute", HandleMuteCommand, rbac::RBAC_PERM_COMMAND_MUTE, Console::Yes },
{ "mutehistory", HandleMuteInfoCommand, rbac::RBAC_PERM_COMMAND_MUTEHISTORY, Console::Yes },
{ "unmute", HandleUnmuteCommand, rbac::RBAC_PERM_COMMAND_UNMUTE, Console::Yes },
@@ -2448,6 +2453,265 @@ public:
return true;
}
static bool HandleRespawnCreatureByGuidCommand(ChatHandler* handler, ObjectGuid::LowType spawnId)
{
CreatureData const* creData = sObjectMgr->GetCreatureData(spawnId);
if (!creData)
{
handler->SendErrorMessage(LANG_RESPAWN_GUID_CREATURE_NOT_FOUND, spawnId);
return false;
}
Map* map = nullptr;
if (handler->GetSession())
{
Player* player = handler->GetSession()->GetPlayer();
if (player->GetMapId() == creData->mapid)
map = player->GetMap();
}
if (!map)
map = sMapMgr->FindMap(creData->mapid, 0);
if (!map)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, creData->mapid);
return true;
}
// First pass: check if any instance is alive
bool isAlive = false;
auto const creBounds = map->GetCreatureBySpawnIdStore().equal_range(spawnId);
for (auto itr = creBounds.first; itr != creBounds.second; ++itr)
{
if (itr->second->IsAlive())
{
isAlive = true;
break;
}
}
if (isAlive)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_CREATURE_ALIVE, spawnId, creData->id1);
return true;
}
// Second pass: respawn any dead corpses in the world
for (auto itr = creBounds.first; itr != creBounds.second; ++itr)
{
if (itr->second->isDead())
itr->second->Respawn(true);
}
// Also trigger via respawn time queue for fully-removed spawns
if (map->GetCreatureRespawnTime(spawnId) > 0)
{
time_t now = GameTime::GetGameTime().count();
map->SaveCreatureRespawnTime(spawnId, now);
}
handler->PSendSysMessage(LANG_RESPAWN_GUID_CREATURE_QUEUED, spawnId, creData->id1);
return true;
}
static bool HandleRespawnGameObjectByGuidCommand(ChatHandler* handler, ObjectGuid::LowType spawnId)
{
GameObjectData const* goData = sObjectMgr->GetGameObjectData(spawnId);
if (!goData)
{
handler->SendErrorMessage(LANG_RESPAWN_GUID_GAMEOBJECT_NOT_FOUND, spawnId);
return false;
}
Map* map = nullptr;
if (handler->GetSession())
{
Player* player = handler->GetSession()->GetPlayer();
if (player->GetMapId() == goData->mapid)
map = player->GetMap();
}
if (!map)
map = sMapMgr->FindMap(goData->mapid, 0);
if (!map)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, goData->mapid);
return true;
}
// First pass: check if any instance is already active
bool isActive = false;
auto const goBounds = map->GetGameObjectBySpawnIdStore().equal_range(spawnId);
for (auto itr = goBounds.first; itr != goBounds.second; ++itr)
{
if (itr->second->isSpawned())
{
isActive = true;
break;
}
}
if (isActive)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_GAMEOBJECT_ACTIVE, spawnId, goData->id);
return true;
}
// Second pass: respawn inactive objects in the world
for (auto itr = goBounds.first; itr != goBounds.second; ++itr)
itr->second->Respawn();
// Also trigger via respawn time queue for fully-removed spawns
if (map->GetGORespawnTime(spawnId) > 0)
{
time_t now = GameTime::GetGameTime().count();
map->SaveGORespawnTime(spawnId, now);
}
handler->PSendSysMessage(LANG_RESPAWN_GUID_GAMEOBJECT_QUEUED, spawnId, goData->id);
return true;
}
static bool HandleRespawnCreatureByEntryCommand(ChatHandler* handler, uint32 entry, Optional<uint32> mapIdArg, Optional<uint32> instanceIdArg)
{
if (!sObjectMgr->GetCreatureTemplate(entry))
{
handler->SendErrorMessage(LANG_RESPAWN_ENTRY_CREATURE_NOT_FOUND, entry);
return false;
}
Map* map = nullptr;
if (handler->GetSession())
{
// In-game: always use the player's current map
map = handler->GetSession()->GetPlayer()->GetMap();
}
else
{
// Console: mapId required, instanceId optional
if (!mapIdArg)
{
handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP);
return false;
}
map = sMapMgr->FindMap(*mapIdArg, instanceIdArg.value_or(0));
}
if (!map)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, mapIdArg.value_or(0));
return false;
}
time_t now = GameTime::GetGameTime().count();
uint32 count = 0;
// Phase 1: respawn dead corpses that are still tracked in the spawn-id store.
// Collect first to avoid iterator invalidation caused by Respawn().
std::vector<Creature*> deadCreatures;
for (auto const& [spawnId, creature] : map->GetCreatureBySpawnIdStore())
{
CreatureData const* data = sObjectMgr->GetCreatureData(spawnId);
if (!data || data->id1 != entry)
continue;
if (creature->isDead())
deadCreatures.push_back(creature);
}
for (Creature* creature : deadCreatures)
{
creature->Respawn(true);
++count;
}
// Phase 2: set respawn time to now for fully-removed spawns, skipping pools
std::vector<ObjectGuid::LowType> toRespawn;
for (auto const& [spawnId, respawnTime] : map->GetCreatureRespawnTimes())
{
CreatureData const* data = sObjectMgr->GetCreatureData(spawnId);
if (!data || data->id1 != entry)
continue;
if (sPoolMgr->IsPartOfAPool<Creature>(spawnId))
continue;
toRespawn.push_back(spawnId);
}
for (ObjectGuid::LowType spawnId : toRespawn)
{
map->SaveCreatureRespawnTime(spawnId, now);
++count;
}
handler->PSendSysMessage(LANG_RESPAWN_ENTRY_CREATURE_QUEUED, count, entry);
return true;
}
static bool HandleRespawnGameObjectByEntryCommand(ChatHandler* handler, uint32 entry, Optional<uint32> mapIdArg, Optional<uint32> instanceIdArg)
{
if (!sObjectMgr->GetGameObjectTemplate(entry))
{
handler->SendErrorMessage(LANG_RESPAWN_ENTRY_GAMEOBJECT_NOT_FOUND, entry);
return false;
}
Map* map = nullptr;
if (handler->GetSession())
{
// In-game: always use the player's current map
map = handler->GetSession()->GetPlayer()->GetMap();
}
else
{
// Console: mapId required, instanceId optional
if (!mapIdArg)
{
handler->SendSysMessage(LANG_LIST_RESPAWNS_NO_MAP);
return false;
}
map = sMapMgr->FindMap(*mapIdArg, instanceIdArg.value_or(0));
}
if (!map)
{
handler->PSendSysMessage(LANG_RESPAWN_GUID_MAP_NOT_LOADED, mapIdArg.value_or(0));
return false;
}
time_t now = GameTime::GetGameTime().count();
uint32 count = 0;
// Phase 1: respawn inactive objects that are still tracked in the spawn-id store.
// Collect first to avoid iterator invalidation caused by Respawn().
std::vector<GameObject*> inactiveGOs;
for (auto const& [spawnId, go] : map->GetGameObjectBySpawnIdStore())
{
GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId);
if (!data || data->id != entry)
continue;
if (!go->isSpawned())
inactiveGOs.push_back(go);
}
for (GameObject* go : inactiveGOs)
{
go->Respawn();
++count;
}
// Phase 2: set respawn time to now for fully-removed spawns, skipping pools
std::vector<ObjectGuid::LowType> toRespawn;
for (auto const& [spawnId, respawnTime] : map->GetGORespawnTimes())
{
GameObjectData const* data = sObjectMgr->GetGameObjectData(spawnId);
if (!data || data->id != entry)
continue;
if (sPoolMgr->IsPartOfAPool<GameObject>(spawnId))
continue;
toRespawn.push_back(spawnId);
}
for (ObjectGuid::LowType spawnId : toRespawn)
{
map->SaveGORespawnTime(spawnId, now);
++count;
}
handler->PSendSysMessage(LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED, count, entry);
return true;
}
static bool HandleRespawnCommand(ChatHandler* handler)
{
Player* player = handler->GetSession()->GetPlayer();
@@ -77,18 +77,6 @@ struct boss_moroes : public BossAI
_activeGuests = 0;
}
void InitializeAI() override
{
BossAI::InitializeAI();
InitializeGuests();
}
void JustReachedHome() override
{
BossAI::JustReachedHome();
InitializeGuests();
}
void InitializeGuests()
{
if (!me->IsAlive())
@@ -96,16 +84,18 @@ struct boss_moroes : public BossAI
if (_activeGuests == 0)
{
_activeGuests |= 0x3F;
_activeGuests = 0x3F;
uint8 rand1 = RAND(0x01, 0x02, 0x04);
uint8 rand2 = RAND(0x08, 0x10, 0x20);
_activeGuests &= ~(rand1 | rand2);
}
for (uint8 i = 0; i < MAX_GUEST_COUNT; ++i)
uint8 positionIndex = 0;
for (uint8 i = 0; i < MAX_GUEST_COUNT && positionIndex < ACTIVE_GUEST_COUNT; ++i)
{
if ((1 << i) & _activeGuests)
{
me->SummonCreature(GuestEntries[i], GuestsPosition[summons.size()], TEMPSUMMON_MANUAL_DESPAWN);
me->SummonCreature(GuestEntries[i], GuestsPosition[positionIndex++], TEMPSUMMON_MANUAL_DESPAWN);
}
}
@@ -130,6 +120,8 @@ struct boss_moroes : public BossAI
_recentlySpoken = false;
_vanished = false;
InitializeGuests();
ScheduleHealthCheckEvent(30, [&] {
DoCastSelf(SPELL_FRENZY, true);
});
@@ -203,6 +195,9 @@ struct boss_moroes : public BossAI
}
}
if (guestList.empty())
return nullptr;
return Acore::Containers::SelectRandomContainerElement(guestList);
}
@@ -211,12 +206,12 @@ struct boss_moroes : public BossAI
bool guestsInRoom = true;
summons.DoForAllSummons([&guestsInRoom](WorldObject* summon)
{
if ((summon->ToCreature()->GetPositionX()) < -11028.f || (summon->ToCreature()->GetPositionY()) < -1955.f) //boundaries of the two doors
Creature* creature = summon->ToCreature();
if (creature->IsAlive() &&
((creature->GetPositionX() < -11028.f) || (creature->GetPositionY() < -1955.f))) // boundaries of the two doors
{
guestsInRoom = false;
return false;
}
return true;
});
return guestsInRoom;
@@ -229,10 +224,6 @@ struct boss_moroes : public BossAI
if (!CheckGuestsInRoom())
{
EnterEvadeMode();
summons.DoForAllSummons([](WorldObject* summon)
{
summon->ToCreature()->DespawnOnEvade(5s);
});
return;
}
@@ -204,7 +204,9 @@ public:
case EVENT_COMPLETE:
{
DoCast(me, SPELL_IDOM_ROOM_CAMERA_SHAKE);
me->SummonGameObject(GO_BELNISTRASZS_BRAZIER, 2577.196f, 947.0781f, 53.16757f, 2.356195f, 0, 0, 0.9238796f, 0.3826832f, 3600);
// Summon via the map so the brazier is not tied to Belnistrasz's
// m_gameObj list and survives his despawn below.
me->GetMap()->SummonGameObject(GO_BELNISTRASZS_BRAZIER, 2577.196f, 947.0781f, 53.16757f, 2.356195f, 0, 0, 0.9238796f, 0.3826832f, 3600);
std::list<WorldObject*> ClusterList;
Acore::AllWorldObjectsInRange objects(me, 50.0f);
Acore::WorldObjectListSearcher<Acore::AllWorldObjectsInRange> searcher(me, ClusterList, objects);
@@ -223,7 +225,7 @@ public:
}
}
instance->SetData(GO_BELNISTRASZS_BRAZIER, DONE);
me->DespawnOrUnsummon();
me->DespawnOrUnsummon(5s);
break;
}
case EVENT_FIREBALL:
@@ -36,8 +36,8 @@ enum Data
// Non-boss data (SetData/GetData only)
DATA_INTRO,
DATA_FROSTSWORN_GENERAL,
DATA_BATTERED_HILT = 6, //For SAI DB
DATA_LK_INTRO,
DATA_BATTERED_HILT,
DATA_WAVE_NUMBER,
DATA_LK_BATTLE,
DATA_SHIP_CAPTAIN,
@@ -60,7 +60,6 @@ public:
case 5:
_owner.SetFacingTo(2.82f);
_owner.SetStandState(UNIT_STAND_STATE_KNEEL);
_owner.m_Events.AddEventAtOffset(new UtherBatteredHiltEvent(_owner, 6), 3s);
break;
case 6:
if (InstanceScript* inst = _owner.GetInstanceScript())
@@ -155,6 +155,7 @@ struct boss_gal_darah : public BossAI
{
Talk(SAY_DEATH);
BossAI::JustDied(killer);
instance->DoRemoveAurasDueToSpellOnPlayers(SPELL_PUNCTURE);
}
void KilledUnit(Unit*) override
@@ -133,6 +133,7 @@ enum Events
EVENT_UNBOUND_PLAGUE,
EVENT_MALLEABLE_GOO,
EVENT_CHOKING_GAS_BOMB,
EVENT_MUTATED_PLAGUE,
};
#define EVENT_GROUP_ABILITIES 1
@@ -653,6 +654,11 @@ public:
me->CastSpell(me, SPELL_CHOKING_GAS_BOMB, false);
events.ScheduleEvent(EVENT_CHOKING_GAS_BOMB, 35s, 40s, EVENT_GROUP_ABILITIES);
break;
case EVENT_MUTATED_PLAGUE:
if (Unit* target = me->GetVictim())
me->CastSpell(target, SPELL_MUTATED_PLAGUE, false);
events.ScheduleEvent(EVENT_MUTATED_PLAGUE, 10s, EVENT_GROUP_ABILITIES);
break;
default:
break;
}
@@ -721,6 +727,7 @@ public:
case 2:
_phase = 3;
events.CancelEvent(EVENT_UNSTABLE_EXPERIMENT);
events.ScheduleEvent(EVENT_MUTATED_PLAGUE, 10s, EVENT_GROUP_ABILITIES);
break;
default:
break;
@@ -441,7 +441,6 @@ struct boss_kologarn : public BossAI
{
events.ScheduleEvent(EVENT_FOCUSED_EYEBEAM, 20s);
me->CastSpell(me, SPELL_FOCUSED_EYEBEAM_SUMMON, false);
Talk(EMOTE_EYES);
return;
}
case EVENT_RESTORE_ARM_LEFT:
@@ -605,6 +604,7 @@ struct boss_kologarn_eyebeam : public ScriptedAI
if (Creature* cr = _instance->GetCreature(BOSS_KOLOGARN))
{
me->CastSpell(cr, me->GetEntry() == NPC_EYE_LEFT ? SPELL_FOCUSED_EYEBEAM_LEFT : SPELL_FOCUSED_EYEBEAM_RIGHT, true);
cr->AI()->Talk(EMOTE_EYES, player);
}
}
}
@@ -71,7 +71,6 @@ enum Actions
ACTION_START_ENCOUNTER = 1,
ACTION_DRAKE_BREATH = 2,
ACTION_PHASE2 = 3,
ACTION_HARPOON_HIT = 4,
};
enum CombatPhase
@@ -171,8 +170,6 @@ public:
_events.Reset();
_summons.DespawnAll();
_phase = PHASE_GROUND;
_harpoonHit = 0;
_loveSkadi = 0;
_firstWaveSummoned = false;
_encounterStarted = false;
@@ -266,8 +263,6 @@ public:
_events.ScheduleEvent(EVENT_SKADI_RESET_CHECK, 6s);
break;
case ACTION_DRAKE_BREATH:
if (_loveSkadi == 1)
++_loveSkadi;
Talk(SAY_DRAKE_BREATH);
break;
case ACTION_PHASE2:
@@ -283,21 +278,9 @@ public:
_events.ScheduleEvent(EVENT_SKADI_SPEAR, 11s);
_events.ScheduleEvent(EVENT_SKADI_WHIRLWIND, 23s);
break;
case ACTION_HARPOON_HIT:
++_harpoonHit;
if (_harpoonHit == 1)
_loveSkadi = 1;
break;
}
}
uint32 GetData(uint32 id) const override
{
if (id == DATA_LOVE_TO_SKADI)
return _loveSkadi;
return 0;
}
void UpdateAI(uint32 diff) override
{
_events.Update(diff);
@@ -384,8 +367,6 @@ public:
EventMap _events;
SummonList _summons;
CombatPhase _phase;
uint8 _harpoonHit;
uint8 _loveSkadi;
bool _firstWaveSummoned;
bool _encounterStarted;
};
@@ -415,6 +396,7 @@ public:
me->SetReactState(REACT_PASSIVE);
me->SetSpeedRate(MOVE_RUN, 2.5f);
_flyingToSide = false;
_passFreshStart = false;
}
void DoAction(int32 param) override
@@ -430,13 +412,6 @@ public:
}
}
void SpellHit(Unit* /*caster*/, SpellInfo const* spellInfo) override
{
if (spellInfo->Id == SPELL_LAUNCH_HARPOON)
if (Creature* skadi = _instance->GetCreature(DATA_SKADI_THE_RUTHLESS))
skadi->AI()->DoAction(ACTION_HARPOON_HIT);
}
void MovementInform(uint32 type, uint32 /*id*/) override
{
if (type == ESCORT_MOTION_TYPE && me->movespline->Finalized())
@@ -470,6 +445,7 @@ public:
FORCED_MOVEMENT_RUN);
break;
case EVENT_GRAUF_BREATH_START:
_passFreshStart = me->IsFullHealth();
me->GetMotionMaster()->MovePath(
_lastSide == POINT_LEFT ? PATH_LEFT : PATH_RIGHT,
FORCED_MOVEMENT_RUN);
@@ -493,10 +469,11 @@ public:
{
skadi->ExitVehicle();
skadi->AI()->DoAction(ACTION_PHASE2);
if (skadi->AI()->GetData(DATA_LOVE_TO_SKADI) == 1)
_instance->SetData(DATA_SKADI_ACHIEVEMENT, true);
}
if (_passFreshStart)
_instance->SetData(DATA_SKADI_ACHIEVEMENT, true);
me->DespawnOrUnsummon(6s);
}
@@ -506,6 +483,7 @@ public:
SummonList _summons;
uint8 _lastSide = POINT_LEFT;
bool _flyingToSide = false;
bool _passFreshStart = false;
};
};
@@ -313,6 +313,7 @@ class spell_hun_taming_the_beast : public AuraScript
if (Creature* creature = target->ToCreature())
{
creature->GetThreatMgr().ClearAllThreat();
creature->GetCombatManager().EndAllCombat();
}
}
}
+16
View File
@@ -20,6 +20,7 @@
#include "Guild.h"
#include "Log.h"
#include "PlayerScript.h"
#include "World.h"
class ChatLogScript : public PlayerScript
{
@@ -38,6 +39,9 @@ public:
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 lang, std::string& msg) override
{
if (!sWorld->getBoolConfig(CONFIG_CHATLOG_ENABLED))
return true;
std::string logType = "";
std::string chatType = "";
@@ -67,6 +71,9 @@ public:
bool OnPlayerCanUseChat(Player* player, uint32 /*type*/, uint32 lang, std::string& msg, Player* receiver) override
{
if (!sWorld->getBoolConfig(CONFIG_CHATLOG_ENABLED))
return true;
//! NOTE:
//! LANG_ADDON can only be sent by client in "PARTY", "RAID", "GUILD", "BATTLEGROUND", "WHISPER"
std::string logType = (lang != LANG_ADDON) ? "chat." : "chat.addon.";
@@ -80,6 +87,9 @@ public:
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 lang, std::string& msg, Group* group) override
{
if (!sWorld->getBoolConfig(CONFIG_CHATLOG_ENABLED))
return true;
//! NOTE:
//! LANG_ADDON can only be sent by client in "PARTY", "RAID", "GUILD", "BATTLEGROUND", "WHISPER"
std::string logType = (lang != LANG_ADDON) ? "chat." : "chat.addon.";
@@ -116,6 +126,9 @@ public:
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 lang, std::string& msg, Guild* guild) override
{
if (!sWorld->getBoolConfig(CONFIG_CHATLOG_ENABLED))
return true;
//! NOTE:
//! LANG_ADDON can only be sent by client in "PARTY", "RAID", "GUILD", "BATTLEGROUND", "WHISPER"
std::string logType = (lang != LANG_ADDON) ? "chat." : "chat.addon.";
@@ -141,6 +154,9 @@ public:
bool OnPlayerCanUseChat(Player* player, uint32 /*type*/, uint32 /*lang*/, std::string& msg, Channel* channel) override
{
if (!sWorld->getBoolConfig(CONFIG_CHATLOG_ENABLED))
return true;
bool isSystem = channel &&
(channel->HasFlag(CHANNEL_FLAG_TRADE) ||
channel->HasFlag(CHANNEL_FLAG_GENERAL) ||