From b9f2d9e5dd639a55e14ebb71a769e74fa826bc65 Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Sun, 12 Jul 2026 05:27:40 -0400 Subject: [PATCH 01/67] chore(Core/Battlegrounds): Remove arena ready marker, add hook for module to replace it. (#26562) --- .../pending_db_world/arena-ready-marker.sql | 3 + .../game/Battlegrounds/Battleground.cpp | 50 +------------- src/server/game/Battlegrounds/Battleground.h | 7 +- .../Battlegrounds/Zones/BattlegroundBE.cpp | 5 +- .../game/Battlegrounds/Zones/BattlegroundBE.h | 4 +- .../Battlegrounds/Zones/BattlegroundDS.cpp | 5 +- .../game/Battlegrounds/Zones/BattlegroundDS.h | 4 +- .../Battlegrounds/Zones/BattlegroundNA.cpp | 5 +- .../game/Battlegrounds/Zones/BattlegroundNA.h | 4 +- .../Battlegrounds/Zones/BattlegroundRL.cpp | 5 +- .../game/Battlegrounds/Zones/BattlegroundRL.h | 4 +- .../Battlegrounds/Zones/BattlegroundRV.cpp | 7 +- .../game/Battlegrounds/Zones/BattlegroundRV.h | 69 +++++++++---------- .../ScriptDefines/AllBattlegroundScript.cpp | 5 ++ .../ScriptDefines/AllBattlegroundScript.h | 8 +++ src/server/game/Scripting/ScriptMgr.h | 1 + src/server/scripts/World/go_scripts.cpp | 15 ---- 17 files changed, 64 insertions(+), 137 deletions(-) create mode 100644 data/sql/updates/pending_db_world/arena-ready-marker.sql diff --git a/data/sql/updates/pending_db_world/arena-ready-marker.sql b/data/sql/updates/pending_db_world/arena-ready-marker.sql new file mode 100644 index 000000000..bf1cd4405 --- /dev/null +++ b/data/sql/updates/pending_db_world/arena-ready-marker.sql @@ -0,0 +1,3 @@ +DELETE FROM `gameobject_template` WHERE `entry` = 301337; +DELETE FROM `gameobject_template_addon` WHERE `entry` = 301337; +DELETE FROM `gameobject_template_locale` WHERE `entry` = 301337; diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index b10feb7b8..828f1f5fa 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -18,12 +18,7 @@ #include "Battleground.h" #include "ArenaSpectator.h" #include "ArenaTeam.h" -#include "BattlegroundBE.h" -#include "BattlegroundDS.h" #include "BattlegroundMgr.h" -#include "BattlegroundNA.h" -#include "BattlegroundRL.h" -#include "BattlegroundRV.h" #include "Chat.h" #include "ChatTextBuilder.h" #include "Creature.h" @@ -533,6 +528,8 @@ inline void Battleground::_ProcessJoin(uint32 diff) // Mark setup as completed m_SetupCompleted = true; + + sScriptMgr->OnBattlegroundSetup(this); } // First announcement at 120s or 60s (Depending on BG or Arena and configured time) @@ -558,33 +555,6 @@ inline void Battleground::_ProcessJoin(uint32 diff) if (StartMessageIds[BG_STARTING_EVENT_THIRD]) SendBroadcastText(StartMessageIds[BG_STARTING_EVENT_THIRD], CHAT_MSG_BG_SYSTEM_NEUTRAL); - - if (isArena()) - switch (GetBgTypeID()) - { - case BATTLEGROUND_NA: - DelObject(BG_NA_OBJECT_READY_MARKER_1); - DelObject(BG_NA_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_BE: - DelObject(BG_BE_OBJECT_READY_MARKER_1); - DelObject(BG_BE_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_RL: - DelObject(BG_RL_OBJECT_READY_MARKER_1); - DelObject(BG_RL_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_DS: - DelObject(BG_DS_OBJECT_READY_MARKER_1); - DelObject(BG_DS_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_RV: - DelObject(BG_RV_OBJECT_READY_MARKER_1); - DelObject(BG_RV_OBJECT_READY_MARKER_2); - break; - default: - break; - } } // Delay expired (after configured prep time) else if (GetStartDelayTime() <= 0 && !(m_Events & BG_STARTING_EVENT_4)) @@ -1380,22 +1350,6 @@ void Battleground::SpectatorsSendPacket(WorldPacket& data) (*itr)->SendDirectMessage(&data); } -void Battleground::ReadyMarkerClicked(Player* p) -{ - if (!isArena() || GetStatus() >= STATUS_IN_PROGRESS || GetStartDelayTime() <= BG_START_DELAY_15S || (m_Events & BG_STARTING_EVENT_3) || p->IsSpectator()) - return; - readyMarkerClickedSet.insert(p->GetGUID()); - uint32 count = readyMarkerClickedSet.size(); - uint32 req = ArenaTeam::GetReqPlayersForType(GetArenaType()); - ChatHandler(p->GetSession()).SendNotification("You are marked as ready {}/{}", count, req); - if (count == req) - { - m_Events |= BG_STARTING_EVENT_2; - m_StartTime += GetStartDelayTime() - BG_START_DELAY_15S; - SetStartDelayTime(BG_START_DELAY_15S); - } -} - void Battleground::BuildPvPLogDataPacket(WorldPacket& data) { uint8 type = (isArena() ? 1 : 0); diff --git a/src/server/game/Battlegrounds/Battleground.h b/src/server/game/Battlegrounds/Battleground.h index 6f9d79e07..34f349af3 100644 --- a/src/server/game/Battlegrounds/Battleground.h +++ b/src/server/game/Battlegrounds/Battleground.h @@ -283,7 +283,6 @@ enum BGHonorMode }; #define ARENA_TIMELIMIT_POINTS_LOSS -16 -#define ARENA_READY_MARKER_ENTRY 301337 /* This class is used to: @@ -387,6 +386,9 @@ public: void ModifyStartDelayTime(int32 diff) { m_StartDelayTime -= diff; } void SetStartDelayTime(int32 Time) { m_StartDelayTime = Time; } + [[nodiscard]] uint8 GetStartingEventFlags() const { return m_Events; } + void AddStartingEventFlag(uint8 flag) { m_Events |= flag; } + void SetMaxPlayersPerTeam(uint32 MaxPlayers) { m_MaxPlayersPerTeam = MaxPlayers; } void SetMinPlayersPerTeam(uint32 MinPlayers) { m_MinPlayersPerTeam = MinPlayers; } @@ -419,9 +421,6 @@ public: [[nodiscard]] BattlegroundPlayerMap const& GetPlayers() const { return m_Players; } [[nodiscard]] uint32 GetPlayersSize() const { return m_Players.size(); } - void ReadyMarkerClicked(Player* p); // pussywizard - GuidSet readyMarkerClickedSet; // pussywizard - typedef std::unordered_map BattlegroundScoreMap; typedef std::unordered_map ArenaLogEntryDataMap; // pussywizard ArenaLogEntryDataMap ArenaLogEntries; // pussywizard diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp index a467d3032..2a8064439 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp @@ -104,10 +104,7 @@ bool BattlegroundBE::SetupBattleground() || !AddObject(BG_BE_OBJECT_DOOR_4, BG_BE_OBJECT_TYPE_DOOR_4, 6177.708f, 227.3481f, 3.604374f, -2.260201f, 0, 0, 0.9044551f, -0.4265689f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_BE_OBJECT_BUFF_1, BG_BE_OBJECT_TYPE_BUFF_1, 6249.042f, 275.3239f, 11.22033f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_BE_OBJECT_BUFF_2, BG_BE_OBJECT_TYPE_BUFF_2, 6228.26f, 249.566f, 11.21812f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_BE_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 6189.47f, 235.54f, 5.52f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_BE_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 6287.19f, 288.25f, 5.33f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_BE_OBJECT_BUFF_2, BG_BE_OBJECT_TYPE_BUFF_2, 6228.26f, 249.566f, 11.21812f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundBE: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundBE.h b/src/server/game/Battlegrounds/Zones/BattlegroundBE.h index abb16edb2..dceafb225 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundBE.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundBE.h @@ -28,9 +28,7 @@ enum BattlegroundBEObjectTypes BG_BE_OBJECT_DOOR_4 = 3, BG_BE_OBJECT_BUFF_1 = 4, BG_BE_OBJECT_BUFF_2 = 5, - BG_BE_OBJECT_READY_MARKER_1 = 6, - BG_BE_OBJECT_READY_MARKER_2 = 7, - BG_BE_OBJECT_MAX = 8 + BG_BE_OBJECT_MAX = 6 }; enum BattlegroundBEGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp index bd3a61f68..064e36478 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp @@ -195,10 +195,7 @@ bool BattlegroundDS::SetupBattleground() // knockback creatures || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_WATERFALL_KNOCKBACK, 1291.76f, 791.02f, 7.115f, 3.054326f, RESPAWN_IMMEDIATELY) || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_1, 1369.977f, 817.2882f, 16.08718f, 3.106686f, RESPAWN_IMMEDIATELY) - || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_2, 1212.833f, 765.3871f, 16.09484f, 0.0f, RESPAWN_IMMEDIATELY) - // Arena Ready Marker - || !AddObject(BG_DS_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 1229.44f, 759.35f, 17.89f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_DS_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 1352.90f, 822.77f, 17.96f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_2, 1212.833f, 765.3871f, 16.09484f, 0.0f, RESPAWN_IMMEDIATELY)) { LOG_ERROR("sql.sql", "BatteGroundDS: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundDS.h b/src/server/game/Battlegrounds/Zones/BattlegroundDS.h index 103ec6cfe..e83beebea 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundDS.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundDS.h @@ -30,9 +30,7 @@ enum BattlegroundDSObjectTypes BG_DS_OBJECT_WATER_2 = 3, BG_DS_OBJECT_BUFF_1 = 4, BG_DS_OBJECT_BUFF_2 = 5, - BG_DS_OBJECT_READY_MARKER_1 = 6, - BG_DS_OBJECT_READY_MARKER_2 = 7, - BG_DS_OBJECT_MAX = 8 + BG_DS_OBJECT_MAX = 6 }; enum BattlegroundDSGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp index fd1ec0bfa..41dc5543a 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp @@ -80,10 +80,7 @@ bool BattlegroundNA::SetupBattleground() || !AddObject(BG_NA_OBJECT_DOOR_4, BG_NA_OBJECT_TYPE_DOOR_4, 4090.064f, 2858.438f, 10.23631f, 0.4928045f, 0, 0, 0.2439165f, 0.9697962f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_NA_OBJECT_BUFF_1, BG_NA_OBJECT_TYPE_BUFF_1, 4009.189941f, 2895.250000f, 13.052700f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_NA_OBJECT_BUFF_2, BG_NA_OBJECT_TYPE_BUFF_2, 4103.330078f, 2946.350098f, 13.051300f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_NA_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 4090.46f, 2875.43f, 12.16f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_NA_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 4022.82f, 2966.61f, 12.17f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_NA_OBJECT_BUFF_2, BG_NA_OBJECT_TYPE_BUFF_2, 4103.330078f, 2946.350098f, 13.051300f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundNA: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundNA.h b/src/server/game/Battlegrounds/Zones/BattlegroundNA.h index 889b9ed0e..0f545c49b 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundNA.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundNA.h @@ -28,9 +28,7 @@ enum BattlegroundNAObjectTypes BG_NA_OBJECT_DOOR_4 = 3, BG_NA_OBJECT_BUFF_1 = 4, BG_NA_OBJECT_BUFF_2 = 5, - BG_NA_OBJECT_READY_MARKER_1 = 6, - BG_NA_OBJECT_READY_MARKER_2 = 7, - BG_NA_OBJECT_MAX = 8 + BG_NA_OBJECT_MAX = 6 }; enum BattlegroundNAGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp index ac845386f..e17ddf734 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp @@ -105,10 +105,7 @@ bool BattlegroundRL::SetupBattleground() || !AddObject(BG_RL_OBJECT_DOOR_2, BG_RL_OBJECT_TYPE_DOOR_2, 1278.648f, 1730.557f, 31.60557f, 1.684245f, 0, 0, 0.7460582f, 0.6658807f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_RL_OBJECT_BUFF_1, BG_RL_OBJECT_TYPE_BUFF_1, 1328.719971f, 1632.719971f, 36.730400f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_RL_OBJECT_BUFF_2, BG_RL_OBJECT_TYPE_BUFF_2, 1243.300049f, 1699.170044f, 34.872601f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_RL_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 1298.61f, 1598.59f, 31.62f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_RL_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 1273.71f, 1734.05f, 31.61f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_RL_OBJECT_BUFF_2, BG_RL_OBJECT_TYPE_BUFF_2, 1243.300049f, 1699.170044f, 34.872601f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundRL: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRL.h b/src/server/game/Battlegrounds/Zones/BattlegroundRL.h index 3e273810c..9a7c63ff3 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRL.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRL.h @@ -26,9 +26,7 @@ enum BattlegroundRLObjectTypes BG_RL_OBJECT_DOOR_2 = 1, BG_RL_OBJECT_BUFF_1 = 2, BG_RL_OBJECT_BUFF_2 = 3, - BG_RL_OBJECT_READY_MARKER_1 = 4, - BG_RL_OBJECT_READY_MARKER_2 = 5, - BG_RL_OBJECT_MAX = 6 + BG_RL_OBJECT_MAX = 4 }; enum BattlegroundRLObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp index 5b77fc9a3..4120a054c 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp @@ -240,12 +240,7 @@ bool BattlegroundRV::SetupBattleground() || !AddObject(BG_RV_OBJECT_PILAR_1, BG_RV_OBJECT_TYPE_PILAR_1, 763.632385f, -306.162384f, 25.909504f, 3.141593f, 0, 0, 0, RESPAWN_IMMEDIATELY) || !AddObject(BG_RV_OBJECT_PILAR_2, BG_RV_OBJECT_TYPE_PILAR_2, 723.644287f, -284.493256f, 24.648525f, 3.141593f, 0, 0, 0, RESPAWN_IMMEDIATELY) || !AddObject(BG_RV_OBJECT_PILAR_3, BG_RV_OBJECT_TYPE_PILAR_3, 763.611145f, -261.856750f, 25.909504f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY) - || !AddObject(BG_RV_OBJECT_PILAR_4, BG_RV_OBJECT_TYPE_PILAR_4, 802.211609f, -284.493256f, 24.648525f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY) - - // Arena Ready Marker - || !AddObject(BG_RV_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 769.93f, -301.04f, 2.80f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_RV_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 757.02f, -267.30f, 2.80f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - ) + || !AddObject(BG_RV_OBJECT_PILAR_4, BG_RV_OBJECT_TYPE_PILAR_4, 802.211609f, -284.493256f, 24.648525f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY)) { LOG_ERROR("sql.sql", "BatteGroundRV: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRV.h b/src/server/game/Battlegrounds/Zones/BattlegroundRV.h index ffa3d5809..38b7c7eb4 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRV.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRV.h @@ -22,53 +22,50 @@ enum BattlegroundRVObjectTypes { - BG_RV_OBJECT_BUFF_1, - BG_RV_OBJECT_BUFF_2, - BG_RV_OBJECT_FIRE_1, - BG_RV_OBJECT_FIRE_2, - BG_RV_OBJECT_FIREDOOR_1, - BG_RV_OBJECT_FIREDOOR_2, + BG_RV_OBJECT_BUFF_1 = 0, + BG_RV_OBJECT_BUFF_2 = 1, + BG_RV_OBJECT_FIRE_1 = 2, + BG_RV_OBJECT_FIRE_2 = 3, + BG_RV_OBJECT_FIREDOOR_1 = 4, + BG_RV_OBJECT_FIREDOOR_2 = 5, - BG_RV_OBJECT_PILAR_1, - BG_RV_OBJECT_PILAR_3, - BG_RV_OBJECT_GEAR_1, - BG_RV_OBJECT_GEAR_2, + BG_RV_OBJECT_PILAR_1 = 6, + BG_RV_OBJECT_PILAR_3 = 7, + BG_RV_OBJECT_GEAR_1 = 8, + BG_RV_OBJECT_GEAR_2 = 9, - BG_RV_OBJECT_PILAR_2, - BG_RV_OBJECT_PILAR_4, - BG_RV_OBJECT_PULLEY_1, - BG_RV_OBJECT_PULLEY_2, + BG_RV_OBJECT_PILAR_2 = 10, + BG_RV_OBJECT_PILAR_4 = 11, + BG_RV_OBJECT_PULLEY_1 = 12, + BG_RV_OBJECT_PULLEY_2 = 13, - BG_RV_OBJECT_ELEVATOR_1, - BG_RV_OBJECT_ELEVATOR_2, + BG_RV_OBJECT_ELEVATOR_1 = 14, + BG_RV_OBJECT_ELEVATOR_2 = 15, - BG_RV_OBJECT_READY_MARKER_1, - BG_RV_OBJECT_READY_MARKER_2, - - BG_RV_OBJECT_MAX, + BG_RV_OBJECT_MAX = 16 }; enum BattlegroundRVGameObjects { - BG_RV_OBJECT_TYPE_BUFF_1 = 184663, - BG_RV_OBJECT_TYPE_BUFF_2 = 184664, - BG_RV_OBJECT_TYPE_FIRE_1 = 192704, - BG_RV_OBJECT_TYPE_FIRE_2 = 192705, + BG_RV_OBJECT_TYPE_BUFF_1 = 184663, + BG_RV_OBJECT_TYPE_BUFF_2 = 184664, + BG_RV_OBJECT_TYPE_FIRE_1 = 192704, + BG_RV_OBJECT_TYPE_FIRE_2 = 192705, - BG_RV_OBJECT_TYPE_FIREDOOR_2 = 192387, - BG_RV_OBJECT_TYPE_FIREDOOR_1 = 192388, - BG_RV_OBJECT_TYPE_PULLEY_1 = 192389, - BG_RV_OBJECT_TYPE_PULLEY_2 = 192390, - BG_RV_OBJECT_TYPE_GEAR_1 = 192393, - BG_RV_OBJECT_TYPE_GEAR_2 = 192394, + BG_RV_OBJECT_TYPE_FIREDOOR_2 = 192387, + BG_RV_OBJECT_TYPE_FIREDOOR_1 = 192388, + BG_RV_OBJECT_TYPE_PULLEY_1 = 192389, + BG_RV_OBJECT_TYPE_PULLEY_2 = 192390, + BG_RV_OBJECT_TYPE_GEAR_1 = 192393, + BG_RV_OBJECT_TYPE_GEAR_2 = 192394, - BG_RV_OBJECT_TYPE_ELEVATOR_1 = 194582, - BG_RV_OBJECT_TYPE_ELEVATOR_2 = 194586, + BG_RV_OBJECT_TYPE_ELEVATOR_1 = 194582, + BG_RV_OBJECT_TYPE_ELEVATOR_2 = 194586, - BG_RV_OBJECT_TYPE_PILAR_1 = 194583, // axe - BG_RV_OBJECT_TYPE_PILAR_2 = 194584, // arena - BG_RV_OBJECT_TYPE_PILAR_3 = 194585, // lightning - BG_RV_OBJECT_TYPE_PILAR_4 = 194587, // ivory + BG_RV_OBJECT_TYPE_PILAR_1 = 194583, // axe + BG_RV_OBJECT_TYPE_PILAR_2 = 194584, // arena + BG_RV_OBJECT_TYPE_PILAR_3 = 194585, // lightning + BG_RV_OBJECT_TYPE_PILAR_4 = 194587, // ivory }; enum BattlegroundRVData diff --git a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp index 9881a25f0..3d9213ed8 100644 --- a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp @@ -104,6 +104,11 @@ void ScriptMgr::OnBattlegroundCreate(Battleground* bg) CALL_ENABLED_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_CREATE, script->OnBattlegroundCreate(bg)); } +void ScriptMgr::OnBattlegroundSetup(Battleground* bg) +{ + CALL_ENABLED_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_SETUP, script->OnBattlegroundSetup(bg)); +} + bool ScriptMgr::CanAddGroupToMatchingPool(BattlegroundQueue* queue, GroupQueueInfo* group, uint32 poolPlayerCount, Battleground* bg, BattlegroundBracketId bracketId) { CALL_ENABLED_BOOLEAN_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_CAN_ADD_GROUP_TO_MATCHING_POOL, !script->CanAddGroupToMatchingPool(queue, group, poolPlayerCount, bg, bracketId)); diff --git a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h index 108f3cf7f..f1aaca7b3 100644 --- a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h +++ b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h @@ -41,6 +41,7 @@ enum AllBattlegroundHook ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_END, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_DESTROY, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_CREATE, + ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_SETUP, ALLBATTLEGROUNDHOOK_CAN_ADD_GROUP_TO_MATCHING_POOL, ALLBATTLEGROUNDHOOK_GET_PLAYER_MATCHMAKING_RATING, ALLBATTLEGROUNDHOOK_END @@ -139,6 +140,13 @@ public: */ virtual void OnBattlegroundCreate(Battleground* /*bg*/) { } + /** + * @brief This hook runs after the Battleground setup is completed (map is created and initial objects are spawned) + * + * @param bg Contains information about the Battleground + */ + virtual void OnBattlegroundSetup(Battleground* /*bg*/) { } + /** * @brief This hook runs before adding a group to the battleground matching pool * diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index d35f71852..cbc540866 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -613,6 +613,7 @@ public: /* BGScript */ void OnBattlegroundEnd(Battleground* bg, TeamId winnerTeamId); void OnBattlegroundDestroy(Battleground* bg); void OnBattlegroundCreate(Battleground* bg); + void OnBattlegroundSetup(Battleground* bg); bool CanAddGroupToMatchingPool(BattlegroundQueue* queue, GroupQueueInfo* group, uint32 poolPlayerCount, Battleground* bg, BattlegroundBracketId bracketId); bool GetPlayerMatchmakingRating(ObjectGuid playerGuid, BattlegroundTypeId bgTypeId, float& outRating); diff --git a/src/server/scripts/World/go_scripts.cpp b/src/server/scripts/World/go_scripts.cpp index cfcf56121..ac8c897e6 100644 --- a/src/server/scripts/World/go_scripts.cpp +++ b/src/server/scripts/World/go_scripts.cpp @@ -75,20 +75,6 @@ public: } }; -class go_arena_ready_marker : public GameObjectScript -{ -public: - go_arena_ready_marker() : GameObjectScript("go_arena_ready_marker") { } - - bool OnGossipHello(Player* player, GameObject* /*go*/) override - { - if (Battleground* bg = player->GetBattleground()) - bg->ReadyMarkerClicked(player); - - return false; - } -}; - /*###### ## go_ethereum_prison ######*/ @@ -1889,7 +1875,6 @@ void AddSC_go_scripts() { new go_seer_of_zebhalak(); new go_witherbark_totem_bundle(); - new go_arena_ready_marker(); new go_ethereum_prison(); new go_ethereum_stasis(); new go_resonite_cask(); From 7e5cb4ac2479a8ce46f5a0f1f996084d6fab19b5 Mon Sep 17 00:00:00 2001 From: sogladev Date: Sun, 12 Jul 2026 11:28:03 +0200 Subject: [PATCH 02/67] fix(Core/Player): explicitly clear CAN_FLY state onlogin (#26560) --- src/server/game/Entities/Player/Player.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 52bee311a..0bd855652 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -11673,6 +11673,12 @@ void Player::SendInitialPacketsAfterAddToMap() auraList.front()->HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); } + // Explicitly synchronize CAN_FLY state with client on login to prevent + // players from retaining flight ability after disconnecting during a + // teleport from a flyable to non-flyable zone (e.g. entering an instance). + if (!HasIncreaseMountedFlightSpeedAura() && !HasFlyAura()) + SetCanFly(false); + // Fix mount, update block gets messed somewhere { if (!isBeingLoaded() && GetMountBlockId() && !HasMountedAura()) From 8f3c8e14534cb25df11df2887eb4b0a5138adcfa Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Sun, 12 Jul 2026 05:28:37 -0400 Subject: [PATCH 03/67] fix(DB/instance): Correct some Gundrak teleport positions based on sniffs. (#26559) --- .../pending_db_world/gundrak-lfg-entrance.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql diff --git a/data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql b/data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql new file mode 100644 index 000000000..fcc608103 --- /dev/null +++ b/data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql @@ -0,0 +1,13 @@ +DELETE FROM `areatrigger_teleport` WHERE `ID` = 5206; +INSERT INTO `areatrigger_teleport` (`ID`, `Name`, `target_map`, `target_position_x`, `target_position_y`, `target_position_z`, `target_orientation`) VALUES +(5206, 'Gundrak (North Entrance)', 604, 1882.32, 631.027, 176.696, 3.1415927); + +-- For below, Wrath Classic (51943) has the player arrive at 1880.74, 853.76, 176.696, 3.14159. +-- However, retail Wrath seems to put the player on the opposing side. +-- The above areatrigger_teleport position was also sniffed from Wrath Classic, so that is being used for the LFG teleport, +-- as it is on the correct side of the instance. + +DELETE FROM `lfg_dungeon_template` WHERE `dungeonId` IN (216, 217); +INSERT INTO `lfg_dungeon_template` (`dungeonId`, `name`, `position_x`, `position_y`, `position_z`, `orientation`, `VerifiedBuild`) VALUES +(216, 'Gundrak', 1882.32, 631.027, 176.696, 3.1415927, 0), -- Normal +(217, 'Gundrak', 1882.32, 631.027, 176.696, 3.1415927, 0); -- Heroic From f073b68b66977ed6592d01b277481057baa7dfbf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Jul 2026 09:28:51 +0000 Subject: [PATCH 04/67] chore(DB): import pending files Referenced commit(s): b9f2d9e5dd639a55e14ebb71a769e74fa826bc65 --- .../arena-ready-marker.sql => db_world/2026_07_12_00.sql} | 1 + .../gundrak-lfg-entrance.sql => db_world/2026_07_12_01.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/arena-ready-marker.sql => db_world/2026_07_12_00.sql} (80%) rename data/sql/updates/{pending_db_world/gundrak-lfg-entrance.sql => db_world/2026_07_12_01.sql} (95%) diff --git a/data/sql/updates/pending_db_world/arena-ready-marker.sql b/data/sql/updates/db_world/2026_07_12_00.sql similarity index 80% rename from data/sql/updates/pending_db_world/arena-ready-marker.sql rename to data/sql/updates/db_world/2026_07_12_00.sql index bf1cd4405..841c39c26 100644 --- a/data/sql/updates/pending_db_world/arena-ready-marker.sql +++ b/data/sql/updates/db_world/2026_07_12_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_11_00 -> 2026_07_12_00 DELETE FROM `gameobject_template` WHERE `entry` = 301337; DELETE FROM `gameobject_template_addon` WHERE `entry` = 301337; DELETE FROM `gameobject_template_locale` WHERE `entry` = 301337; diff --git a/data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql b/data/sql/updates/db_world/2026_07_12_01.sql similarity index 95% rename from data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql rename to data/sql/updates/db_world/2026_07_12_01.sql index fcc608103..6e7b4705a 100644 --- a/data/sql/updates/pending_db_world/gundrak-lfg-entrance.sql +++ b/data/sql/updates/db_world/2026_07_12_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_12_00 -> 2026_07_12_01 DELETE FROM `areatrigger_teleport` WHERE `ID` = 5206; INSERT INTO `areatrigger_teleport` (`ID`, `Name`, `target_map`, `target_position_x`, `target_position_y`, `target_position_z`, `target_orientation`) VALUES (5206, 'Gundrak (North Entrance)', 604, 1882.32, 631.027, 176.696, 3.1415927); From a54220b5f51cce8ab6c3f7b34a1cf2aef3133f62 Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:45:23 -0400 Subject: [PATCH 05/67] fix(DB/Spell): Only proc Hack and Slash with damaging attacks. (#26579) --- data/sql/updates/pending_db_world/hack-and-slash-proc.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 data/sql/updates/pending_db_world/hack-and-slash-proc.sql diff --git a/data/sql/updates/pending_db_world/hack-and-slash-proc.sql b/data/sql/updates/pending_db_world/hack-and-slash-proc.sql new file mode 100644 index 000000000..015540445 --- /dev/null +++ b/data/sql/updates/pending_db_world/hack-and-slash-proc.sql @@ -0,0 +1,3 @@ +DELETE FROM `spell_proc` WHERE `SpellId` = -13960; +INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES +(-13960, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0); From a0b6553b5b83771e5a4a59bf3b68e76c6ce90826 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Jul 2026 23:46:36 +0000 Subject: [PATCH 06/67] chore(DB): import pending files Referenced commit(s): a54220b5f51cce8ab6c3f7b34a1cf2aef3133f62 --- .../hack-and-slash-proc.sql => db_world/2026_07_12_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/hack-and-slash-proc.sql => db_world/2026_07_12_02.sql} (89%) diff --git a/data/sql/updates/pending_db_world/hack-and-slash-proc.sql b/data/sql/updates/db_world/2026_07_12_02.sql similarity index 89% rename from data/sql/updates/pending_db_world/hack-and-slash-proc.sql rename to data/sql/updates/db_world/2026_07_12_02.sql index 015540445..bd1ec74b0 100644 --- a/data/sql/updates/pending_db_world/hack-and-slash-proc.sql +++ b/data/sql/updates/db_world/2026_07_12_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_12_01 -> 2026_07_12_02 DELETE FROM `spell_proc` WHERE `SpellId` = -13960; INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES (-13960, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0); From 80653245d91346f200d098a7416e66fc70980535 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:37:14 -0300 Subject: [PATCH 07/67] fix(Scripts/Ulduar): track Yogg-Saron encounter state and gate phase 2 on engagement (#26581) Co-authored-by: Claude Fable 5 --- .../Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp | 17 ++++++++++++----- .../Northrend/Ulduar/Ulduar/instance_ulduar.cpp | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp index dff9c61ca..08835f46c 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp @@ -469,7 +469,7 @@ struct boss_yoggsaron_sara : public ScriptedAI { _instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, CRITERIA_NOT_GETTING_OLDER); _instance->DoRemoveAurasDueToSpellOnPlayers(SPELL_SANITY); - _instance->SetData(BOSS_YOGGSARON, NOT_STARTED); + _instance->SetBossState(BOSS_YOGGSARON, NOT_STARTED); if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) go->SetGoState(GO_STATE_ACTIVE); } @@ -480,12 +480,11 @@ struct boss_yoggsaron_sara : public ScriptedAI if (!_instance) return; - // some simple hack checks - if (_instance->GetBossState(BOSS_VEZAX) != DONE || _instance->GetBossState(BOSS_XT002) != DONE) + if (_instance->GetBossState(BOSS_VEZAX) != DONE) return; _instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, CRITERIA_NOT_GETTING_OLDER); - _instance->SetData(BOSS_YOGGSARON, IN_PROGRESS); + _instance->SetBossState(BOSS_YOGGSARON, IN_PROGRESS); me->SetInCombatWithZone(); AttackStart(target); @@ -702,6 +701,14 @@ struct boss_yoggsaron_sara : public ScriptedAI void DamageTaken(Unit* who, uint32& damage, DamageEffectType, SpellSchoolMask) override { + // Guardians can be spawned by walking into Ominous Clouds even when InitFight + // never ran (e.g. Vezax not defeated); their novas must not start phase 2 then. + if (!_instance || _instance->GetBossState(BOSS_YOGGSARON) != IN_PROGRESS) + { + damage = 0; + return; + } + if (who && who->GetEntry() == NPC_GUARDIAN_OF_YS && !_secondPhase) { damage = 25000; @@ -1104,7 +1111,7 @@ struct boss_yoggsaron : public ScriptedAI if (_instance) { - _instance->SetData(BOSS_YOGGSARON, DONE); + _instance->SetBossState(BOSS_YOGGSARON, DONE); if (Creature* sara = _instance->GetCreature(DATA_SARA)) sara->AI()->DoAction(ACTION_YOGG_SARON_DEATH); if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 9daa556ff..4cdd6bc11 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -47,7 +47,8 @@ DoorData const doorData[] = { GO_KEEPERS_GATE, BOSS_THORIM, DOOR_TYPE_PASSAGE }, { GO_KEEPERS_GATE, BOSS_FREYA, DOOR_TYPE_PASSAGE }, { GO_VEZAX_DOOR, BOSS_VEZAX, DOOR_TYPE_PASSAGE }, - { GO_YOGG_SARON_DOORS, BOSS_YOGGSARON, DOOR_TYPE_ROOM }, + // GO_YOGG_SARON_DOORS is handled by boss_yoggsaron_sara: it closes 15 seconds + // after the pull, not immediately when the encounter enters IN_PROGRESS. { GO_DOODAD_UL_SIGILDOOR_03, BOSS_ALGALON, DOOR_TYPE_ROOM }, { GO_DOODAD_UL_UNIVERSEFLOOR_01, BOSS_ALGALON, DOOR_TYPE_ROOM }, { GO_DOODAD_UL_UNIVERSEFLOOR_02, BOSS_ALGALON, DOOR_TYPE_SPAWN_HOLE }, From 250628881081baec12e454ed6aa1c6d039f33a55 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:38:06 -0300 Subject: [PATCH 08/67] fix(Scripts/Ulduar): set Algalon encounter state through SetBossState (#26582) Co-authored-by: Claude Fable 5 --- .../Ulduar/Ulduar/boss_algalon_the_observer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp index 0af5863dc..c57e0d6cd 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp @@ -364,7 +364,7 @@ struct boss_algalon_the_observer : public ScriptedAI } if (_instance) - _instance->SetData(BOSS_ALGALON, FAIL); + _instance->SetBossState(BOSS_ALGALON, FAIL); ScriptedAI::EnterEvadeMode(why); } @@ -391,7 +391,7 @@ struct boss_algalon_the_observer : public ScriptedAI } if (_instance) - _instance->SetData(BOSS_ALGALON, NOT_STARTED); + _instance->SetBossState(BOSS_ALGALON, NOT_STARTED); } void KilledUnit(Unit* victim) override @@ -446,7 +446,7 @@ struct boss_algalon_the_observer : public ScriptedAI me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); me->InterruptNonMeleeSpells(false); if (_instance) - _instance->SetData(BOSS_ALGALON, NOT_STARTED); + _instance->SetBossState(BOSS_ALGALON, NOT_STARTED); break; case ACTION_INIT_ALGALON: _firstPull = false; @@ -660,7 +660,7 @@ struct boss_algalon_the_observer : public ScriptedAI brann->AI()->DoAction(ACTION_FINISH_INTRO); break; case EVENT_START_COMBAT: - _instance->SetData(BOSS_ALGALON, IN_PROGRESS); + _instance->SetBossState(BOSS_ALGALON, IN_PROGRESS); Talk(SAY_ALGALON_AGGRO); break; case EVENT_REMOVE_UNNATTACKABLE: @@ -737,7 +737,7 @@ struct boss_algalon_the_observer : public ScriptedAI case EVENT_OUTRO_START: if (_instance) { - _instance->SetData(BOSS_ALGALON, DONE); + _instance->SetBossState(BOSS_ALGALON, DONE); _instance->SetData(DATA_ALGALON_DEFEATED, 1); } break; From 8c3c35e5827db9bd2190156f4daada1a3e5215c4 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:38:38 -0300 Subject: [PATCH 09/67] fix(Scripts/Ulduar): Freya no longer stuck in combat with Hodir's helpers (#26576) Co-authored-by: Claude Opus 4.8 --- .../updates/pending_db_world/rev_1783886287444094700.sql | 4 ++++ src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783886287444094700.sql diff --git a/data/sql/updates/pending_db_world/rev_1783886287444094700.sql b/data/sql/updates/pending_db_world/rev_1783886287444094700.sql new file mode 100644 index 000000000..aa6f10949 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783886287444094700.sql @@ -0,0 +1,4 @@ +-- Hodir helpers (#26330): swap PACIFIED (131072) for IMMUNE_TO_NPC (512) per sniffs, so Freya's +-- instance-wide Ground Tremor can't tag them and block her reset. SWIMMING kept where set; +-- immunity is cleared on Hodir engage (boss_hodir.cpp). +UPDATE `creature_template` SET `unit_flags` = (`unit_flags` & ~131072) | 512 WHERE `entry` IN (32893, 32897, 32900, 32901, 32941, 32946, 32948, 32950, 33325, 33326, 33327, 33328, 33330, 33331, 33332, 33333); diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp index 46f9a3367..f8f406e3b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp @@ -255,6 +255,12 @@ struct boss_hodir : public BossAI events.ScheduleEvent(EVENT_HARD_MODE_MISSED, 3min); Talk(TEXT_AGGRO); + // Helpers spawn IMMUNE_TO_NPC so idle bosses (e.g. Freya's Ground Tremor, #26330) can't tag + // them; drop it on engage. TODO: confirm timing - sniffs may clear it per-helper on unfreeze. + for (uint8 i = 0; i < 8; ++i) + if (Creature* helper = GetHelper(i)) + helper->RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); + if (instance->GetBossState(BOSS_HODIR) != DONE) instance->SetBossState(BOSS_HODIR, IN_PROGRESS); } From 902651c4a645026923b2acb77e24ef2d5e3eade2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Jul 2026 19:39:57 +0000 Subject: [PATCH 10/67] chore(DB): import pending files Referenced commit(s): 8c3c35e5827db9bd2190156f4daada1a3e5215c4 --- .../rev_1783886287444094700.sql => db_world/2026_07_13_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783886287444094700.sql => db_world/2026_07_13_00.sql} (91%) diff --git a/data/sql/updates/pending_db_world/rev_1783886287444094700.sql b/data/sql/updates/db_world/2026_07_13_00.sql similarity index 91% rename from data/sql/updates/pending_db_world/rev_1783886287444094700.sql rename to data/sql/updates/db_world/2026_07_13_00.sql index aa6f10949..94f5fcb58 100644 --- a/data/sql/updates/pending_db_world/rev_1783886287444094700.sql +++ b/data/sql/updates/db_world/2026_07_13_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_12_02 -> 2026_07_13_00 -- Hodir helpers (#26330): swap PACIFIED (131072) for IMMUNE_TO_NPC (512) per sniffs, so Freya's -- instance-wide Ground Tremor can't tag them and block her reset. SWIMMING kept where set; -- immunity is cleared on Hodir engage (boss_hodir.cpp). From 787b46d974d4600c5a77eab2fb1273e5f6dd64ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= Date: Mon, 13 Jul 2026 22:38:58 +0200 Subject: [PATCH 11/67] feat(Agents): add generate-pr-description skill (#26586) --- .agents/README.md | 17 +++++ .../skills/generate-pr-description/SKILL.md | 69 +++++++++++++++++++ .claude/skills/generate-pr-description | 1 + 3 files changed, 87 insertions(+) create mode 100644 .agents/README.md create mode 100644 .agents/skills/generate-pr-description/SKILL.md create mode 120000 .claude/skills/generate-pr-description diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 000000000..395f65b17 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,17 @@ +# .agents + +Source of truth for this repo's agent skills and rules: + +- `skills//SKILL.md` +- `rules/.md` + +Agent-specific directories expose each via a relative symlink — for Claude Code: + + .claude/skills/ -> ../../.agents/skills/ + .claude/rules/.md -> ../../.agents/rules/.md + +To add a skill or rule: create it here, then symlink it. + +The skills in this repository often build on those from +[agent-toolkit](https://github.com/eai-org/agent-toolkit/), invoking them when available. +Installing the toolkit at user level is therefore recommended. diff --git a/.agents/skills/generate-pr-description/SKILL.md b/.agents/skills/generate-pr-description/SKILL.md new file mode 100644 index 000000000..ea7b505cc --- /dev/null +++ b/.agents/skills/generate-pr-description/SKILL.md @@ -0,0 +1,69 @@ +--- +name: generate-pr-description +description: Generate the PR title and description from the current branch changes, following the repo's PR template. +metadata: + version: "1.0" +--- + +# Generate PR description + +Produce a copy-pasteable PR title and body for the current changes, based on the repo's +`pull_request_template.md`. Output text only — never create the PR, push, or run any +state-changing git command. + +If a `use-conversational-language` skill is available, invoke it and follow it when writing the +prose. If not available, fetch it from its [remote source](https://github.com/eai-org/agent-toolkit/blob/main/skills/use-conversational-language/SKILL.md). + +## Gather the changes + +"The changes" is the union of: + +- committed branch work: `git diff ...HEAD`, where `` is `origin/master` — + or `upstream/master` when the checkout is a fork with an `upstream` remote; +- staged and unstaged changes; +- the branch's commit messages (context only). + +If all three are empty, say so and stop. + +## Facts, never assumptions + +Only two sources may fill the description: the diff and facts stated in the conversation. Never +present anything else as fact — no assumed testing, sources, issue links, or TODOs. A section +without backing facts stays empty/unchecked, silently: an empty section is correct, a guessed one +is wrong. + +Ask the user only when unsure about substance — what the change does, which scope the title should +carry, contradictory information. Missing fill-in facts are never a reason to ask. + +## Title + +`type(Scope/Subscope): short description` — imperative, max 50 chars. Types: `feat`, `fix`, +`refactor`, `style`, `docs`, `test`, `chore`; scopes: `Core`, `Scripts`, `DB`. Copy subscope naming +from recent commits touching the same areas (`git log --oneline -- `). + +## Body + +Fill in `pull_request_template.md` (repo root): + +- Strip the instructional `` comments; keep the "How to Test AzerothCore PRs" footer + intact. +- Describe what the changes do and why. +- "Changes Proposed" checkboxes, from diff paths: `src/server/scripts/` → Scripts; `data/sql/` → + Database; other `src/` → Core. +- AI disclosure checkbox: always tick it; name the agent/model running this skill and state + whether AI produced the changes themselves or only this description, per the conversation. +- Issues Addressed, SOURCE, Tests Performed, How to Test, Known Issues/TODO: stated facts only + (see above). + +## Output + +Print exactly: + +~~~ +TITLE: + +PR DESCRIPTION: +```markdown +<body> +``` +~~~ diff --git a/.claude/skills/generate-pr-description b/.claude/skills/generate-pr-description new file mode 120000 index 000000000..3aff0944b --- /dev/null +++ b/.claude/skills/generate-pr-description @@ -0,0 +1 @@ +../../.agents/skills/generate-pr-description \ No newline at end of file From 81a8c779bd16417d6b69390060777b3db7a9f703 Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Tue, 14 Jul 2026 00:36:28 +0200 Subject: [PATCH 12/67] fix(Scripts/Ulduar): Freya repeats Allies of Nature summon order (#26598) --- .../Northrend/Ulduar/Ulduar/boss_freya.cpp | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index 216aa247b..ff07548e3 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -212,6 +212,13 @@ enum Misc WAYPOINT_BLUE = 18, }; +enum AlliesGroup +{ + GROUP_TRIO = 0, + GROUP_CONSERVATOR = 1, + GROUP_LASHERS = 2, +}; + struct boss_freya : public BossAI { boss_freya(Creature* pCreature) : BossAI(pCreature, BOSS_FREYA) @@ -220,9 +227,9 @@ struct boss_freya : public BossAI instance->SetBossState(BOSS_FREYA, DONE); } - uint8 _waveNumber; uint8 _trioKilled; uint8 _spawnedAmount; + uint8 _setPermutation; uint8 _lumberjacked; bool _respawningTrio; bool _backToNature; @@ -248,7 +255,7 @@ struct boss_freya : public BossAI _lumberjacked = 0; _spawnedAmount = 0; _trioKilled = 0; - _waveNumber = urand(1, 3); + _setPermutation = urand(0, 5); _respawningTrio = false; _backToNature = true; _deforestation = 0; @@ -329,37 +336,42 @@ struct boss_freya : public BossAI void SpawnWave() { - _waveNumber = _waveNumber == 1 ? 3 : _waveNumber - 1; Talk(EMOTE_ALLIES_OF_NATURE); - // Wave of three - if (_waveNumber == 1) + static constexpr uint8 permTable[6][3] = { + {GROUP_TRIO, GROUP_CONSERVATOR, GROUP_LASHERS}, + {GROUP_TRIO, GROUP_LASHERS, GROUP_CONSERVATOR}, + {GROUP_CONSERVATOR, GROUP_TRIO, GROUP_LASHERS}, + {GROUP_CONSERVATOR, GROUP_LASHERS, GROUP_TRIO}, + {GROUP_LASHERS, GROUP_TRIO, GROUP_CONSERVATOR}, + {GROUP_LASHERS, GROUP_CONSERVATOR, GROUP_TRIO} + }; + + switch (permTable[_setPermutation][_spawnedAmount % 3]) { - Talk(SAY_SUMMON_TRIO); - me->SummonCreature(NPC_ANCIENT_WATER_SPIRIT, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_STORM_LASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_SNAPLASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - } - // Ancient Conservator - else if (_waveNumber == 2) - { - Talk(SAY_SUMMON_CONSERVATOR); - me->SummonCreature(NPC_ANCIENT_CONSERVATOR, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); - } - // Detonating Lashers - else if (_waveNumber == 3) - { - Talk(SAY_SUMMON_LASHERS); - // Spread the lashers evenly in a ring around Freya instead of - // clustering them in one spot, matching retail. - for (uint8 i = 0; i < 10; ++i) - { - float angle = i * 2.0f * float(M_PI) / 10.0f + frand(-0.35f, 0.35f); - float dist = frand(18.0f, 28.0f); - float x = me->GetPositionX() + dist * std::cos(angle); - float y = me->GetPositionY() + dist * std::sin(angle); - me->SummonCreature(NPC_DETONATING_LASHER, x, y, me->GetMapHeight(x, y, me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); - } + case GROUP_TRIO: + Talk(SAY_SUMMON_TRIO); + me->SummonCreature(NPC_ANCIENT_WATER_SPIRIT, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); + me->SummonCreature(NPC_STORM_LASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); + me->SummonCreature(NPC_SNAPLASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); + break; + case GROUP_CONSERVATOR: // Ancient Conservator + Talk(SAY_SUMMON_CONSERVATOR); + me->SummonCreature(NPC_ANCIENT_CONSERVATOR, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); + break; + case GROUP_LASHERS: // Detonating Lashers + Talk(SAY_SUMMON_LASHERS); + // Spread the lashers evenly in a ring around Freya instead of + // clustering them in one spot, matching retail. + for (uint8 i = 0; i < 10; ++i) + { + float angle = i * 2.0f * float(M_PI) / 10.0f + frand(-0.35f, 0.35f); + float dist = frand(18.0f, 28.0f); + float x = me->GetPositionX() + dist * std::cos(angle); + float y = me->GetPositionY() + dist * std::sin(angle); + me->SummonCreature(NPC_DETONATING_LASHER, x, y, me->GetMapHeight(x, y, me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); + } + break; } } From 178c9c69e6b1b9efb16aa2b9946762c90d42eab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Tue, 14 Jul 2026 17:45:48 +0200 Subject: [PATCH 13/67] feat(Core/Battlegrounds): rework LowLevelsMinPlayersOverride (#26601) --- .../apps/worldserver/worldserver.conf.dist | 25 +++ .../game/Battlegrounds/Battleground.cpp | 7 + src/server/game/Battlegrounds/Battleground.h | 6 +- .../game/Battlegrounds/BattlegroundUtils.cpp | 18 +- .../game/Battlegrounds/BattlegroundUtils.h | 6 + src/server/game/World/WorldConfig.cpp | 6 + src/server/game/World/WorldConfig.h | 6 + .../LowLevelsMinPlayersOverrideTest.cpp | 204 ++++++++++++++++++ 8 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index dd6fe04a3..da7a62806 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -3976,10 +3976,35 @@ Battleground.SpeedBuffRespawn = 150 # # Battleground.Override.LowLevels.MinPlayers # Description: Overrides the minimum number of required players per team for all levels < MaxPlayerLevel +# Used as fallback for any battleground without a per-battleground override (see below) # Default: 0 (Disabled) Battleground.Override.LowLevels.MinPlayers = 0 +# +# Battleground.Override.LowLevels.MinPlayers.AV +# Battleground.Override.LowLevels.MinPlayers.WS +# Battleground.Override.LowLevels.MinPlayers.AB +# Battleground.Override.LowLevels.MinPlayers.EY +# Battleground.Override.LowLevels.MinPlayers.SA +# Battleground.Override.LowLevels.MinPlayers.IC +# Description: Per-battleground overrides of the minimum number of required players per team +# for all levels < MaxPlayerLevel. +# AV = Alterac Valley, WS = Warsong Gulch, AB = Arathi Basin, +# EY = Eye of the Storm, SA = Strand of the Ancients, IC = Isle of Conquest +# Resolution order: per-battleground key, then Battleground.Override.LowLevels.MinPlayers, +# then the battleground_template DB value. +# Matches entered through the Random Battleground queue always use the global +# Battleground.Override.LowLevels.MinPlayers, never the per-battleground keys. +# Default: 0 (Disabled, use Battleground.Override.LowLevels.MinPlayers) + +Battleground.Override.LowLevels.MinPlayers.AV = 0 +Battleground.Override.LowLevels.MinPlayers.WS = 0 +Battleground.Override.LowLevels.MinPlayers.AB = 0 +Battleground.Override.LowLevels.MinPlayers.EY = 0 +Battleground.Override.LowLevels.MinPlayers.SA = 0 +Battleground.Override.LowLevels.MinPlayers.IC = 0 + # # Battleground.Warsong.Flags # Description: Set the number of flags required for a team to win in Warsong battleground diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index 828f1f5fa..caa95df99 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -19,6 +19,7 @@ #include "ArenaSpectator.h" #include "ArenaTeam.h" #include "BattlegroundMgr.h" +#include "BattlegroundUtils.h" #include "Chat.h" #include "ChatTextBuilder.h" #include "Creature.h" @@ -242,6 +243,12 @@ Battleground::~Battleground() delete itr.second; } +uint32 Battleground::GetMinPlayersPerTeam() const +{ + uint32 lowLevelsOverride = GetLowLevelsMinPlayersOverride(GetBgTypeID()); + return (lowLevelsOverride && !isTemplate() && !isMaxLevel() && !isArena()) ? lowLevelsOverride : m_MinPlayersPerTeam; +} + void Battleground::Update(uint32 diff) { // pussywizard: diff --git a/src/server/game/Battlegrounds/Battleground.h b/src/server/game/Battlegrounds/Battleground.h index 34f349af3..3c42603c6 100644 --- a/src/server/game/Battlegrounds/Battleground.h +++ b/src/server/game/Battlegrounds/Battleground.h @@ -347,11 +347,7 @@ public: } [[nodiscard]] uint32 GetMaxPlayersPerTeam() const { return m_MaxPlayersPerTeam; } - [[nodiscard]] uint32 GetMinPlayersPerTeam() const - { - auto lowLevelsOverride = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); - return (lowLevelsOverride && !isTemplate() && !isMaxLevel() && !isArena()) ? lowLevelsOverride : m_MinPlayersPerTeam; - } + [[nodiscard]] uint32 GetMinPlayersPerTeam() const; [[nodiscard]] int32 GetStartDelayTime() const { return m_StartDelayTime; } [[nodiscard]] uint8 GetArenaType() const { return m_ArenaType; } diff --git a/src/server/game/Battlegrounds/BattlegroundUtils.cpp b/src/server/game/Battlegrounds/BattlegroundUtils.cpp index 54703472b..f0c4b476c 100644 --- a/src/server/game/Battlegrounds/BattlegroundUtils.cpp +++ b/src/server/game/Battlegrounds/BattlegroundUtils.cpp @@ -14,7 +14,23 @@ uint32 GetMinPlayersPerTeam(Battleground* bg, PvPDifficultyEntry const* bracketE auto maxPlayerLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); auto isMaxLevel = bracketEntry->minLevel <= maxPlayerLevel && maxPlayerLevel <= bracketEntry->maxLevel; - auto lowLevelsOverride = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); + auto lowLevelsOverride = GetLowLevelsMinPlayersOverride(bg->GetBgTypeID()); return (lowLevelsOverride && !isMaxLevel) ? lowLevelsOverride : bg->GetMinPlayersPerTeam(); } + +uint32 GetLowLevelsMinPlayersOverride(BattlegroundTypeId bgTypeId) +{ + uint32 perBg = 0; + switch (bgTypeId) + { + case BATTLEGROUND_AV: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV); break; + case BATTLEGROUND_WS: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS); break; + case BATTLEGROUND_AB: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB); break; + case BATTLEGROUND_EY: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY); break; + case BATTLEGROUND_SA: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA); break; + case BATTLEGROUND_IC: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC); break; + default: break; + } + return perBg ? perBg : sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); +} diff --git a/src/server/game/Battlegrounds/BattlegroundUtils.h b/src/server/game/Battlegrounds/BattlegroundUtils.h index 9d18cbb82..ed54548f7 100644 --- a/src/server/game/Battlegrounds/BattlegroundUtils.h +++ b/src/server/game/Battlegrounds/BattlegroundUtils.h @@ -5,4 +5,10 @@ uint32 GetMinPlayersPerTeam(Battleground* bg, PvPDifficultyEntry const* bracketEntry); +// Effective low-levels MinPlayersPerTeam override for a BG type: +// the per-BG config if set, else the global one. 0 = no override. +// BGs entered through the Random BG queue have BATTLEGROUND_RB as +// their type, so they always resolve through the global key. +uint32 GetLowLevelsMinPlayersOverride(BattlegroundTypeId bgTypeId); + #endif // BATTLEGROUNDUTILS_H diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 5233d6e48..26ca792c0 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -426,6 +426,12 @@ void WorldConfig::BuildConfigCache() SetConfigValue<uint32>(CONFIG_BATTLEGROUND_PREP_TIME, "Battleground.PrepTime", 120); SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, "Battleground.Override.LowLevels.MinPlayers", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, "Battleground.Override.LowLevels.MinPlayers.AV", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, "Battleground.Override.LowLevels.MinPlayers.WS", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, "Battleground.Override.LowLevels.MinPlayers.AB", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, "Battleground.Override.LowLevels.MinPlayers.EY", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, "Battleground.Override.LowLevels.MinPlayers.SA", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, "Battleground.Override.LowLevels.MinPlayers.IC", 0); SetConfigValue<bool>(CONFIG_BATTLEGROUND_DISABLE_QUEST_SHARE_IN_BG, "Battleground.DisableQuestShareInBG", false); SetConfigValue<bool>(CONFIG_BATTLEGROUND_DISABLE_READY_CHECK_IN_BG, "Battleground.DisableReadyCheckInBG", false); SetConfigValue<bool>(CONFIG_BATTLEGROUND_CAST_DESERTER, "Battleground.CastDeserter", true); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index 7e3ef1319..e869d8b3b 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -264,6 +264,12 @@ enum ServerConfigs CONFIG_DISABLE_BREATHING, CONFIG_BATTLEGROUND_PREP_TIME, CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_SPAM_DELAY, CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_TIMER, CONFIG_BATTLEGROUND_PREMATURE_FINISH_TIMER, diff --git a/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp b/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp new file mode 100644 index 000000000..3073f1374 --- /dev/null +++ b/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp @@ -0,0 +1,204 @@ +/* + * 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 "Battleground.h" +#include "BattlegroundUtils.h" +#include "DBCStructure.h" +#include "ScriptMgr.h" +#include "ScriptDefines/AllBattlegroundScript.h" +#include "WorldMock.h" +#include "gtest/gtest.h" + +/** + * Tests the resolution order of the low-levels MinPlayersPerTeam override: + * per-BG config key > global config key > battleground_template value, + * and the guards that keep it away from arenas, templates (member function) + * and max-level brackets. + */ +class LowLevelsMinPlayersOverrideTest : public ::testing::Test +{ +protected: + static constexpr uint32 TemplateMinPlayers = 5; + + void SetUp() override + { + // Battleground's destructor fires OnBattlegroundDestroy; the hook + // registry must be sized before any hook is invoked. + ScriptRegistry<BGScript>::InitEnabledHooksIfNeeded(ALLBATTLEGROUNDHOOK_END); + + previousWorld_ = std::move(sWorld); + worldMock_ = new ::testing::NiceMock<WorldMock>(); + ON_CALL(*worldMock_, getIntConfig(::testing::_)) + .WillByDefault(::testing::Return(0)); + ON_CALL(*worldMock_, getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + .WillByDefault(::testing::Return(80)); + sWorld.reset(worldMock_); + } + + void TearDown() override + { + sWorld = std::move(previousWorld_); + } + + void SetIntConfig(ServerConfigs index, uint32 value) + { + ON_CALL(*worldMock_, getIntConfig(index)) + .WillByDefault(::testing::Return(value)); + } + + static PvPDifficultyEntry MakeBracket(uint32 minLevel, uint32 maxLevel) + { + return PvPDifficultyEntry(/*mapId*/ 0, /*bracketId*/ 0, minLevel, maxLevel, /*difficulty*/ 0); + } + + // A template BG as loaded from battleground_template + static void SetupTemplate(Battleground& bg, BattlegroundTypeId bgTypeId) + { + bg.SetBgTypeID(bgTypeId); + bg.SetMinPlayersPerTeam(TemplateMinPlayers); + } + + // A real BG instance bound to a level bracket + static void SetupRealBg(Battleground& bg, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const& bracket) + { + SetupTemplate(bg, bgTypeId); + bg.SetBracket(&bracket); + } + + ::testing::NiceMock<WorldMock>* worldMock_ = nullptr; + std::unique_ptr<IWorld> previousWorld_; +}; + +TEST_F(LowLevelsMinPlayersOverrideTest, PerBgKeyWinsOverGlobalAndTemplate) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), 2u); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), 2u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, GlobalKeyAppliesWhenPerBgUnset) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), 4u); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), 4u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, TemplateValueAppliesWhenNoOverrideSet) +{ + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, MaxLevelBracketIgnoresOverride) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(71, 80); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); + + // same via the template path of the free helper + Battleground templateBg; + SetupTemplate(templateBg, BATTLEGROUND_WS); + EXPECT_EQ(GetMinPlayersPerTeam(&templateBg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, ArenaIgnoresOverride) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_NA, bracket); + bg.SetArenaorBGType(true); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, PerBgKeyDoesNotLeakToOtherBgs) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_WS), 2u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AB), 0u); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_AB, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, EachPerBgKeyResolvesIndependently) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, 10); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, 3); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, 5); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, 6); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AV), 10u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_WS), 2u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AB), 3u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_EY), 4u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_SA), 5u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_IC), 6u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, UnmappedBgTypeFallsBackToGlobal) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_RB), 4u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, FreeHelperOnTemplateUsesPerBgValue) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground templateBg; + SetupTemplate(templateBg, BATTLEGROUND_WS); + PvPDifficultyEntry bracket = MakeBracket(10, 19); + + // the free helper resolves via the bracket entry for templates + EXPECT_EQ(GetMinPlayersPerTeam(&templateBg, &bracket), 2u); + + // the member function never applies overrides to templates + EXPECT_EQ(templateBg.GetMinPlayersPerTeam(), TemplateMinPlayers); +} From a1a3c4e56268f0c022d13879096f45bcfbd642e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Tue, 14 Jul 2026 17:47:00 +0200 Subject: [PATCH 14/67] docs: compact CLAUDE.md (#26234) --- CLAUDE.md | 50 +++++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7e6e9031d..0d4c0b76b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,12 @@ AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK) ## Agent rules -- **Do not configure or build unless explicitly asked.** Builds are slow (CMake + compile of a large C++ codebase) and rarely needed to make code changes. -- **Never edit SQL files outside `data/sql/updates/pending_db_*/`.** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable (do not modify). -- **Do not run git commands that modify repo state** (commit, branch, merge, rebase, reset, push, …) unless explicitly requested, and do not include them in plans. Read-only git (status, diff, log) is fine. +- **Do not configure or build unless explicitly asked.** Builds are slow and rarely needed for code changes. +- **Never edit SQL files outside `data/sql/updates/pending_db_*/` unless explicitly requested. ** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable. ## Build -Out-of-source build is required (in-source is blocked by CMake). +Out-of-source build is required (in-source is blocked). ```bash mkdir -p build && cd build @@ -19,9 +18,9 @@ cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithD make -j$(nproc) && make install ``` -Compiler: **C++20** required (`CMAKE_CXX_STANDARD 20`). Useful CMake flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full flag set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. +C++20 required (`CMAKE_CXX_STANDARD 20`). Useful flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. -Tests (Google Test, in `src/test/`): configure with `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. +Tests (Google Test, in `src/test/`): configure `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. ## Repository layout @@ -34,18 +33,15 @@ Tests (Google Test, in `src/test/`): configure with `-DBUILD_TESTING=ON`, then ` - `src/test/` — Google Test unit tests + mocks. - `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored). - `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`. -- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts (see below). +- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts. - `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored. - `deps/` — vendored third-party dependencies. ## Adding SQL updates 1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`). -2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` you write into. -3. Required SQL conventions (enforced by `apps/codestyle/codestyle-sql.py`): - - Every `INSERT` must be preceded by a matching `DELETE` (idempotency). - - 4-space indent (no tabs), trailing newline, no double semicolons, no multiple blank lines. - - Tables must use the InnoDB engine. +2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` to write into. +3. Conventions enforced by `apps/codestyle/codestyle-sql.py`: every `INSERT` preceded by a matching `DELETE` (idempotency); no double semicolons; no multiple blank lines; InnoDB engine. The three databases: @@ -55,6 +51,8 @@ The three databases: ## Code style +Formatting (charset, indent width, line length, final newline, trailing whitespace) follows `.editorconfig`. + Run the linters before claiming a change is done: ```bash @@ -62,7 +60,7 @@ python apps/codestyle/codestyle-cpp.py # C++ python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master) ``` -Hard rules (also enforced by CI with `-Werror`): +Hard rules (also enforced by CI with `-Werror`, plus `cppcheck`): - 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 120 cols, trailing newline. - Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`. @@ -75,30 +73,28 @@ Hard rules (also enforced by CI with `-Werror`): - `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`. - `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`. -CI also runs `cppcheck`. - ## Project conventions -- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`, `LOG_ERROR`, `LOG_DEBUG`, `LOG_TRACE`). Categories are hierarchical, dot-separated (e.g. `server.loading`, `entities.player`, `sql.dev`). No `printf`-style; no `sLog->`; no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. -- **Random**: use project helpers from `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Do not use `std::rand` or `<random>` directly. -- **Strings**: `Acore::StringFormat(fmt, args...)` (wraps `fmt::format`, `{}` placeholders) — `src/common/Utilities/StringFormat.h`. -- **Config**: read options with `sConfigMgr->GetOption<T>("Name", default)`. -- **Namespace**: project-wide is `Acore::` (no `Trinity::` remnants — agents porting from upstream forks must rename). -- **Long-lived references**: do not store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `ObjectAccessor::GetCreature(*from, guid)`, `Map::GetCreature(guid)`, etc. -- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums) rather than raw query strings. Reads that don't need to block the world tick go through the async path: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback` for non-prepared). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. -- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation). Both are members of `CreatureAI`; see any boss script under `src/server/scripts/` for examples — don't roll your own tick counters. +- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`/`ERROR`/`DEBUG`/`TRACE`). Categories are hierarchical, dot-separated (`server.loading`, `entities.player`, `sql.dev`). No `printf`-style, no `sLog->`, no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. +- **Random**: use helpers in `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Not `std::rand` or `<random>`. +- **Strings**: `Acore::StringFormat(fmt, args...)` (`{}` placeholders) — `src/common/Utilities/StringFormat.h`. +- **Config**: `sConfigMgr->GetOption<T>("Name", default)`. +- **Namespace**: project-wide `Acore::` (no `Trinity::` remnants — rename when porting from upstream forks). +- **Long-lived references**: don't store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `Map::GetCreature(guid)`, etc. +- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums), not raw query strings. Non-blocking reads go async: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback`). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. +- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation), both members of `CreatureAI` — don't roll your own tick counters. See any boss script under `src/server/scripts/`. ## Scripting registration Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist: -- **Spell / aura scripts**: use the `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) macro inside `AddSC_<name>()`. +- **Spell / aura scripts**: `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) inside `AddSC_<name>()`. - **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern. -Then declare and call `AddSC_<name>()` from the regional loader: `Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, etc. +Then declare and call `AddSC_<name>()` from the regional loader (`Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, …). -**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table — not in C++. Engine: `src/server/game/AI/SmartScripts/`. For new creature behaviour prefer SmartAI (added via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. +**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table, not C++ (engine: `src/server/game/AI/SmartScripts/`). For new creature behaviour prefer SmartAI (via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. -**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro where one exists) inside `AddSC_<name>()`. Full hook list: https://www.azerothcore.org/wiki/hooks-script. +**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro) inside `AddSC_<name>()`. Full list: https://www.azerothcore.org/wiki/hooks-script. Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored). From 32dab337d065c31e58160765c280890900641cef Mon Sep 17 00:00:00 2001 From: Vox <ndinosaur1213@Live.com> Date: Tue, 14 Jul 2026 12:20:53 -0400 Subject: [PATCH 15/67] fix(db/Creature): Ebon Hold Ambient Improvements Part 2: Command Center (Upper Floor) (#26580) --- .../rev_1783891171377876900.sql | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783891171377876900.sql diff --git a/data/sql/updates/pending_db_world/rev_1783891171377876900.sql b/data/sql/updates/pending_db_world/rev_1783891171377876900.sql new file mode 100644 index 000000000..a2872bf80 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783891171377876900.sql @@ -0,0 +1,94 @@ +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 2: Command Center (Bottom Floor) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Faction corrections +-- Siouxsie the Banshee (Entry 27928) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 27928; +-- Squire Edwards (Entry 28486) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 28486; +-- Coldwraith (Entry 28488) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 28488; +-- Disciple of Frost (Entry 28490) +UPDATE `creature_template` SET `faction`= 2082 WHERE `entry`= 28490; +-- Lord Thorval (Entry 29196) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 29196; +-- Knight of the Ebon Blade (Entry 31094) (part of the DK questline, is phased, but here nonetheless) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 31094; +-- Knight of the Ebon Blade (Entry 29202) - Command Center floor - weapon sheathing +DELETE FROM `creature_addon` WHERE `guid` IN (125719, 125727, 125730, 125733, 125738, 125743, 125749, 125753); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125719, 0, 0, 0, 0, 0, 0, NULL), +(125727, 0, 0, 0, 0, 0, 0, NULL), +(125730, 0, 0, 0, 0, 0, 0, NULL), +(125733, 0, 0, 0, 0, 0, 0, NULL), +(125738, 0, 0, 0, 0, 0, 0, NULL), +(125743, 0, 0, 0, 0, 0, 0, NULL), +(125749, 0, 0, 0, 0, 0, 0, NULL), +(125753, 0, 0, 0, 0, 0, 0, NULL); +-- Knight of the Ebon Blade (Entry 29202, GUIDs 125728-729) - Command Center OUTER ring patrol +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125728; +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 0 WHERE `guid`= 125729; + +DELETE FROM `creature_addon` WHERE `guid`= 125728; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125728, 1257280, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257280; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257280, 1, 2453.019, -5659.009, 376.963, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 2, 2436.503, -5654.292, 377.010, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 3, 2421.870, -5646.072, 377.085, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 4, 2409.462, -5633.826, 377.061, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 5, 2401.033, -5619.471, 377.059, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 6, 2396.028, -5602.503, 377.042, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 7, 2395.134, -5584.744, 377.032, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 8, 2400.252, -5568.880, 377.175, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 9, 2408.467, -5554.637, 377.087, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 10, 2422.047, -5542.582, 377.077, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 11, 2436.357, -5533.727, 377.048, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 12, 2453.023, -5528.724, 377.021, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 13, 2470.619, -5528.724, 376.969, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 14, 2486.896, -5534.060, 377.074, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 15, 2500.929, -5542.755, 377.070, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 16, 2511.579, -5556.004, 377.029, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 17, 2520.007, -5569.621, 377.088, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 18, 2525.179, -5586.072, 377.027, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 19, 2525.346, -5603.292, 376.955, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 20, 2521.025, -5618.274, 377.021, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 21, 2512.638, -5633.113, 377.022, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 22, 2500.469, -5644.090, 377.130, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 23, 2485.997, -5652.299, 377.080, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 24, 2470.307, -5658.005, 377.048, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Follower 125729 +DELETE FROM `creature_formations` WHERE `leaderGUID`= 125728 OR `memberGUID` IN (125728, 125729); +INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES +(125728, 125728, 0, 0, 7, 0, 0), +(125728, 125729, 3, 270, 519, 0, 0); +-- Knight of the Ebon Blade (Entry 29202, GUID 125746) - Command Center INNER ring patrol +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125746; + +DELETE FROM `creature_addon` WHERE `guid`= 125746; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125746, 1257460, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257460; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257460, 1, 2476.547, -5569.105, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 2, 2465.888, -5564.635, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 3, 2454.430, -5564.898, 366.996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 4, 2439.936, -5573.309, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 5, 2431.440, -5587.982, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 6, 2431.727, -5599.489, 367.319, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 7, 2436.017, -5610.170, 367.033, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 8, 2442.707, -5616.534, 367.034, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 9, 2446.977, -5619.203, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 10, 2456.497, -5622.578, 366.988, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 11, 2465.452, -5622.340, 367.009, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 12, 2468.838, -5621.344, 367.016, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 13, 2476.673, -5618.066, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 14, 2482.256, -5612.358, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 15, 2489.440, -5599.080, 367.042, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 16, 2488.702, -5586.393, 367.230, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 17, 2485.971, -5579.744, 367.045, NULL, 0, 0, 0, 0, 0, 100, 0); From 856bf1443ee1f638c4f38d308d6490f8593f36a5 Mon Sep 17 00:00:00 2001 From: Vox <ndinosaur1213@Live.com> Date: Tue, 14 Jul 2026 12:21:14 -0400 Subject: [PATCH 16/67] fix(db/Creature): Ebon Hold Ambient Improvements Part 1: Training Room (Lower Floor) (#26574) --- .../rev_1783819497054020200.sql | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783819497054020200.sql diff --git a/data/sql/updates/pending_db_world/rev_1783819497054020200.sql b/data/sql/updates/pending_db_world/rev_1783819497054020200.sql new file mode 100644 index 000000000..0d07b2b75 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783819497054020200.sql @@ -0,0 +1,246 @@ +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 1: Training Room (Upper Floor) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Corpulous (Entry 29205, GUID 125759) +-- Spawn, home anchor, waypoints +UPDATE `creature` SET `position_x`= 2438.5864, `position_y`= -5554.151, `position_z`= 420.79996, `orientation`= 3.9704, `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125759; + +DELETE FROM `creature_addon` WHERE `guid`= 125759; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125759, 1257590, 0, 0, 0, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257590; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257590, 1, 2438.5864, -5554.151, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 2, 2417.4072, -5577.2603, 420.76572, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 3, 2418.5613, -5602.91, 420.8, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 4, 2430.862, -5626.988, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 5, 2462.9768, -5636.701, 420.6738, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 6, 2484.44, -5626.886, 420.42795, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 7, 2500.5364, -5595.214, 420.70547, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 8, 2492.1143, -5571.9556, 420.5543, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 9, 2468.1182, -5553.14, 420.66876, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Knight of the Ebon Blade (Entry 29202, GUIDs 125750-751) +-- Patrol pair +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125750; + +DELETE FROM `creature_addon` WHERE `guid`= 125750; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125750, 1257500, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257500; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257500, 1, 2433.57, -5553.8066, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 2, 2420.6594, -5566.9326, 420.63525, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 3, 2413.44, -5584.155, 420.79105, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 4, 2413.6936, -5602.8584, 420.8, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 5, 2420.8923, -5620.4087, 420.9104, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 6, 2433.7537, -5633.345, 420.73993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 7, 2450.98, -5640.4243, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 8, 2469.9941, -5640.2007, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 9, 2487.1394, -5633.4414, 420.70834, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 10, 2500.1155, -5620.1206, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 11, 2507.2742, -5602.695, 420.84003, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 12, 2507.081, -5584.0166, 420.79993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 13, 2500.0461, -5566.8726, 420.79993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 14, 2486.89, -5553.5205, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 15, 2469.6587, -5546.532, 420.6999, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 16, 2450.987, -5546.5557, 420.70724, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Follower 125751 (formation: follows 125750) +DELETE FROM `creature_formations` WHERE `leaderGUID`= 125750 OR `memberGUID` IN (125750, 125751); +INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES +(125750, 125750, 0, 0, 7, 0, 0), +(125750, 125751, 3, 90, 519, 0, 0); +-- Knight of the Ebon Blade (Entry 29202, GUID 125754) +-- Idle, no wander +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 0 WHERE `guid`= 125754; +-- Risen Drudge (Entry 29212, dead decorative corpses) +-- Make the dead-posed drudges (StandState 7) untargettable: NOT_SELECTABLE (0x2000000) | template flags (32784) = 33587216 +UPDATE `creature` SET `unit_flags`= 33587216 WHERE `guid` IN (125766, 125768, 125770, 125771, 125773, 125774, 125775, 125776, 125779, 125780, 125781, 125784, 125786, 125788, 125789, 125790, 125792, 125793, 125794, 125795, 125796, 125797, 125799, 125800, 125801, 125803, 125804, 125805, 125807, 125808, 125809, 125810); +-- Risen Drudge (Entry 29212) - remove 2 AC-only spawns absent from the retail sniff +DELETE FROM `creature_addon` WHERE `guid` IN (125767, 125806); +DELETE FROM `creature` WHERE `guid` IN (125767, 125806); +-- Knight of the Ebon Blade (Entry 29202) +-- Sheath weapons +DELETE FROM `creature_addon` WHERE `guid` IN (125651, 125718, 125737, 125742, 125744, 125745, 125748, 125752, 125754, 125755); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125651, 0, 0, 0, 0, 0, 0, NULL), +(125718, 0, 0, 0, 0, 0, 0, NULL), +(125737, 0, 0, 0, 0, 0, 0, NULL), +(125742, 0, 0, 0, 0, 0, 0, NULL), +(125744, 0, 0, 0, 0, 0, 0, NULL), +(125745, 0, 0, 0, 0, 0, 0, NULL), +(125748, 0, 0, 0, 0, 0, 0, NULL), +(125752, 0, 0, 0, 0, 0, 0, NULL), +(125754, 0, 0, 0, 0, 0, 0, NULL), +(125755, 0, 0, 0, 0, 0, 0, NULL); +-- Knight of the Ebon Blade (Entry 29202) - hidden in the rafters (Z~444) +DELETE FROM `creature_addon` WHERE `guid` IN (125714, 125716, 125721, 125732, 125734, 125735, 125739, 125740); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125714, 0, 0, 0, 0, 0, 0, NULL), +(125716, 0, 0, 0, 0, 0, 0, NULL), +(125721, 0, 0, 0, 0, 0, 0, NULL), +(125732, 0, 0, 0, 0, 0, 0, NULL), +(125734, 0, 0, 0, 0, 0, 0, NULL), +(125735, 0, 0, 0, 0, 0, 0, NULL), +(125739, 0, 0, 0, 0, 0, 0, NULL), +(125740, 0, 0, 0, 0, 0, 0, NULL); +-- Vigilant Gargoyle (Entry 29239) - all 8 (Training Room airspace) +UPDATE `creature_template` SET `AIName`= 'SmartAI' WHERE `entry`= 29239; + +UPDATE `creature` SET `position_x`= 2434.2053, `position_y`= -5662.7285, `position_z`= 445.16962, `orientation`= 1.17073, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125826; +UPDATE `creature` SET `position_x`= 2386.5322, `position_y`= -5591.9404, `position_z`= 445.1513, `orientation`= 6.21624, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125827; +UPDATE `creature` SET `position_x`= 2391.4082, `position_y`= -5567.293, `position_z`= 444.41574, `orientation`= 5.88485, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125828; +UPDATE `creature` SET `position_x`= 2483.3416, `position_y`= -5565.2993, `position_z`= 465.3641, `orientation`= 5.16390, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125829; +UPDATE `creature` SET `position_x`= 2451.695, `position_y`= -5646.18, `position_z`= 434.86523, `orientation`= 2.34530, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125830; +UPDATE `creature` SET `position_x`= 2430.0688, `position_y`= -5548.5366, `position_z`= 444.9371, `orientation`= 5.23599, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125831; +UPDATE `creature` SET `position_x`= 2458.673, `position_y`= -5667.2334, `position_z`= 445.40192, `orientation`= 1.52346, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125832; +UPDATE `creature` SET `position_x`= 2488.3079, `position_y`= -5584.386, `position_z`= 481.03842, `orientation`= 2.61030, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125833; + +DELETE FROM `creature_addon` WHERE `guid` IN (125826, 125827, 125828, 125829, 125830, 125831, 125832, 125833); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125826, 1258260, 0, 9, 1, 444, 0, NULL), +(125827, 1258270, 0, 9, 1, 444, 0, NULL), +(125828, 1258280, 0, 9, 1, 444, 0, NULL), +(125829, 1258290, 0, 0, 1, 444, 0, NULL), +(125830, 1258300, 0, 0, 1, 444, 0, NULL), +(125831, 1258310, 0, 9, 1, 444, 0, NULL), +(125832, 1258320, 0, 9, 1, 444, 0, NULL), +(125833, 1258330, 0, 0, 1, 444, 0, NULL); +-- Flight paths: patrollers +DELETE FROM `waypoint_data` WHERE `id` IN (1258260, 1258270, 1258280, 1258290, 1258300, 1258310, 1258320, 1258330); +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1258260, 1, 2434.21, -5662.73, 445.17, 1.17073, 5, 65000, 0, 2, 0, 100, 0), +(1258260, 2, 2437.7, -5653.26, 447.963, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 3, 2449.12, -5599.83, 455.546, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 4, 2463.19, -5545.83, 455.546, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 5, 2462.16, -5519.81, 445.019, 4.71029, 5, 90000, 0, 2, 0, 100, 0), +(1258260, 6, 2461.56, -5528.22, 448.031, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 7, 2456.53, -5572.52, 454.475, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 8, 2441, -5638.43, 454.475, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 1, 2386.53, -5591.94, 445.151, 6.21624, 5, 100000, 0, 2, 0, 100, 0), +(1258270, 2, 2396.07, -5594.14, 447.163, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 3, 2436.67, -5610.5, 451.023, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 4, 2473.23, -5633.96, 451.023, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 5, 2490.76, -5638.42, 445.051, 2.17773, 5, 70000, 0, 2, 0, 100, 0), +(1258270, 6, 2482.04, -5634.05, 447.43, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 7, 2437.04, -5610.4, 455.235, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 8, 2406.09, -5597.51, 451.596, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 1, 2391.41, -5567.29, 444.416, 5.88485, 5, 82000, 0, 2, 0, 100, 0), +(1258280, 2, 2398.94, -5570.2, 446.666, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 3, 2440.29, -5586.84, 449.832, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 4, 2505.82, -5612.69, 449.832, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 5, 2529.11, -5619.67, 445.193, 2.81575, 5, 108000, 0, 2, 0, 100, 0), +(1258280, 6, 2523.4, -5615.98, 445.721, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 7, 2457.17, -5592.02, 452.471, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 8, 2417.3, -5576.37, 452.471, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258290, 1, 2483.34, -5565.3, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 2, 2496.26, -5592, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 3, 2487.79, -5622.76, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 4, 2454.34, -5633.35, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 5, 2425.15, -5610.25, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 6, 2428.72, -5575.9, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 7, 2455.77, -5556.59, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 1, 2451.7, -5646.18, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 2, 2439.38, -5633.58, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 3, 2435.8, -5612.65, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 4, 2415.5, -5592.08, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 5, 2420.02, -5564.28, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 6, 2442.14, -5561.77, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 7, 2455.83, -5546.95, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 8, 2473.27, -5540.89, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 9, 2485.81, -5558.36, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 10, 2486.56, -5585.89, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 11, 2512.01, -5598.65, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 12, 2498.43, -5621.79, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 13, 2473.17, -5621.09, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 14, 2464.31, -5642.48, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258310, 1, 2430.07, -5548.54, 444.937, 5.23599, 5, 95000, 0, 3, 0, 100, 0), +(1258310, 2, 2440.2, -5553.77, 447.152, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 3, 2479.28, -5579.33, 458.901, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 4, 2508.33, -5596.56, 458.901, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 5, 2534.25, -5595.29, 445.207, 3.49066, 5, 60000, 0, 3, 0, 100, 0), +(1258310, 6, 2531.36, -5596.21, 447.298, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 7, 2486.9, -5597.17, 448.131, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 8, 2441.01, -5584.44, 448.52, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 1, 2458.67, -5667.23, 445.402, 1.52346, 5, 75000, 0, 2, 0, 100, 0), +(1258320, 2, 2461.16, -5656.92, 448.499, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 3, 2478.7, -5619.14, 450.054, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 4, 2495.67, -5585.25, 450.054, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 5, 2504.88, -5563.37, 444.887, 3.80665, 5, 105000, 0, 2, 0, 100, 0), +(1258320, 6, 2499.79, -5572.24, 447.68, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 7, 2471.41, -5619.1, 454.346, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258330, 1, 2488.31, -5584.39, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 2, 2454.49, -5564.5, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 3, 2435.28, -5578, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 4, 2431.28, -5602.2, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 5, 2446.33, -5619.11, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 6, 2468.88, -5620.94, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 7, 2486.97, -5604.07, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0); +-- SmartAI (5 patrollers) +DELETE FROM `smart_scripts` WHERE `entryorguid` IN (-125826, -125827, -125828, -125831, -125832) AND `source_type`= 0; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(-125826, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125826, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125826, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125826, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125827, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125827, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125827, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125827, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125828, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125828, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125828, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125828, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125831, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125831, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125831, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125831, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125832, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125832, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125832, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125832, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'); +-- Faction correction for vendors +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry` IN (28500, 29203, 29205, 29207, 29208); +-- Master Siegesmith Corvus (Entry 28500, GUID 125869) +UPDATE `creature_template` SET `AIName`= 'SmartAI' WHERE `entry`= 28500; +UPDATE `creature` SET `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125869; + +DELETE FROM `creature_addon` WHERE `guid`= 125869; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125869, 1258690, 0, 0, 1, 233, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1258690; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +-- 16-point weave loop (close-range sniff); ~22-32s craft pause at the 4 anvils (pt 5,7,10,16) +(1258690, 1, 2438.5435, -5634.903, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 2, 2449.8833, -5641.199, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 3, 2460.378, -5650.1074, 420.8016, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 4, 2467.7244, -5654.838, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 5, 2466.9749, -5656.5464, 420.79996, NULL, 0, 22000, 0, 0, 0, 100, 0), +(1258690, 6, 2440.863, -5647.832, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 7, 2433.588, -5649.348, 420.79996, NULL, 0, 32000, 0, 0, 0, 100, 0), +(1258690, 8, 2449.2322, -5647.929, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 9, 2456.4922, -5647.385, 420.77432, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 10, 2466.7793, -5648.771, 420.79996, NULL, 0, 28000, 0, 0, 0, 100, 0), +(1258690, 11, 2455.2024, -5647.0728, 420.78763, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 12, 2447.792, -5646.166, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 13, 2439.7908, -5641.177, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 14, 2435.9255, -5634.6216, 420.72903, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 15, 2432.0063, -5633.902, 420.7254, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 16, 2431.6282, -5635.6772, 420.65244, NULL, 0, 25000, 0, 0, 0, 100, 0); + +DELETE FROM `creature_text` WHERE `CreatureID`= 28500; +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(28500, 0, 0, 'First, he gives away all of my precious Saronite, then he demands I begin crafting Runeblades of even greater power! Pah!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 1, 'Fix my armor, Corvus! My blade is rusty, Corvus! ... when will these damned mortals ever learn to care for themselves?', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 2, 'I may not have any mindless servants, but at least I can see a quality blade made from time to time.', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 3, 'There is work to be done here, but do you see anyone jumping to help me? No! Wretches, all of them!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 4, 'These Death Knights truly know no honor. First, they take my greatest arms and armor, crafted with care, forged with the Lich King''s blessing. Then they wander off to who knows where and come back dressed like Goblins, mumbling something about pawning off their armor for a few coppers! Pathetic!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 5, 'What is this nonsense about the Light? I have weapons and armor to make!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'); + +DELETE FROM `smart_scripts` WHERE `entryorguid`= 28500 AND `source_type`= 0; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(28500, 0, 0, 0, 1, 0, 100, 0, 2100000, 2700000, 2100000, 2700000, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Corvus - ambient line at random ~40min'); From 7f8db197e6d458144cf7d46d5ea5c875a81937ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:22:04 +0000 Subject: [PATCH 17/67] chore(DB): import pending files Referenced commit(s): 32dab337d065c31e58160765c280890900641cef --- .../rev_1783819497054020200.sql => db_world/2026_07_14_00.sql} | 1 + .../rev_1783891171377876900.sql => db_world/2026_07_14_01.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1783819497054020200.sql => db_world/2026_07_14_00.sql} (99%) rename data/sql/updates/{pending_db_world/rev_1783891171377876900.sql => db_world/2026_07_14_01.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1783819497054020200.sql b/data/sql/updates/db_world/2026_07_14_00.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1783819497054020200.sql rename to data/sql/updates/db_world/2026_07_14_00.sql index 0d07b2b75..c82cbc2ae 100644 --- a/data/sql/updates/pending_db_world/rev_1783819497054020200.sql +++ b/data/sql/updates/db_world/2026_07_14_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_13_00 -> 2026_07_14_00 -- ------------------------------------------- -- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) -- Ebon Hold Re-haul Part 1: Training Room (Upper Floor) diff --git a/data/sql/updates/pending_db_world/rev_1783891171377876900.sql b/data/sql/updates/db_world/2026_07_14_01.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1783891171377876900.sql rename to data/sql/updates/db_world/2026_07_14_01.sql index a2872bf80..74608d8c1 100644 --- a/data/sql/updates/pending_db_world/rev_1783891171377876900.sql +++ b/data/sql/updates/db_world/2026_07_14_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_14_00 -> 2026_07_14_01 -- ------------------------------------------- -- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) -- Ebon Hold Re-haul Part 2: Command Center (Bottom Floor) From bddaeeff944571e692e6a95beadab08494b8e430 Mon Sep 17 00:00:00 2001 From: Vox <ndinosaur1213@Live.com> Date: Tue, 14 Jul 2026 14:29:01 -0400 Subject: [PATCH 18/67] fix(db/Creature): Ebon Hold Ambient Improvements Part 3: Lord Thorval Lecture (#26608) --- .../rev_1784028882869393000.sql | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784028882869393000.sql diff --git a/data/sql/updates/pending_db_world/rev_1784028882869393000.sql b/data/sql/updates/pending_db_world/rev_1784028882869393000.sql new file mode 100644 index 000000000..d67d1e5e4 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784028882869393000.sql @@ -0,0 +1,48 @@ +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 3: Lord Thorval (Walking Lecture) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Lord Thorval (Entry 29196, GUID 125712) +UPDATE `creature` SET `position_x`= 2527.7344, `position_y`= -5549.2075, `position_z`= 377.18475, `orientation`= 3.735, `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125712; + +DELETE FROM `creature_addon` WHERE `guid`= 125712; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125712, 1257120, 0, 0, 1, 0, 0, NULL); +-- Waypoints (22 min between lectures/walking cycle) +DELETE FROM `waypoint_data` WHERE `id`= 1257120; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257120, 1, 2527.7344, -5549.2075, 377.18475, 3.735, 0, 1320000, 0, 0, 0, 100, 0), +(1257120, 2, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 8000, 0, 0, 0, 100, 0), +(1257120, 3, 2530.72, -5555.91, 377.13217, 3.263766, 0, 6500, 0, 0, 0, 100, 0), +(1257120, 4, 2530.4983, -5555.5771, 377.135, 3.263766, 0, 8500, 0, 0, 0, 100, 0), +(1257120, 5, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 6400, 0, 0, 0, 100, 0), +(1257120, 6, 2523.2881, -5544.7523, 377.219, 4.433136, 0, 8500, 0, 0, 0, 100, 0), +(1257120, 7, 2530.72, -5555.91, 377.13217, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 8, 2523.0664, -5544.4194, 377.22162, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 9, 2530.72, -5555.91, 377.13217, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 10, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 10000, 0, 0, 0, 100, 0); +-- Speech +DELETE FROM `creature_text` WHERE (`CreatureID` = 29196) AND (`GroupID` IN (0, 1, 2, 3, 4, 5, 6, 7, 8)); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(29196, 0, 0, 'As disciples of blood, you strive to master the very lifeforce of your enemies.', 12, 0, 100, 1, 0, 0, 29867, 0, 'Lord Thorval Lecture 1'), +(29196, 1, 0, 'Be it by blade or incantation, blood feeds our attacks and weakens our foes.', 12, 0, 100, 1, 0, 0, 29868, 0, 'Lord Thorval Lecture 2'), +(29196, 2, 0, 'True masters learn to make blood serve more than just their strength in battle.', 12, 0, 100, 1, 0, 0, 29869, 0, 'Lord Thorval Lecture 3'), +(29196, 3, 0, 'Stripping energy from our foes, both fighting and fallen, allows us to persevere where lesser beings fall exhausted.', 12, 0, 100, 1, 0, 0, 29870, 0, 'Lord Thorval Lecture 4'), +(29196, 4, 0, 'And every foe that falls, energy sapped and stolen, only further fuels our assault.', 12, 0, 100, 1, 0, 0, 29871, 0, 'Lord Thorval Lecture 5'), +(29196, 5, 0, 'As masters of blood, we know battle without end...', 12, 0, 100, 1, 0, 0, 29872, 0, 'Lord Thorval Lecture 6'), +(29196, 6, 0, 'We know hunger never to be quenched...', 12, 0, 100, 1, 0, 0, 29873, 0, 'Lord Thorval Lecture 7'), +(29196, 7, 0, 'We know power never to be overcome...', 12, 0, 100, 1, 0, 0, 29874, 0, 'Lord Thorval Lecture 8'), +(29196, 8, 0, 'As masters of blood, we are masters of life and death itself. Against us, even hope falls drained and lifeless.', 12, 0, 100, 1, 0, 0, 29875, 0, 'Lord Thorval Lecture 9'); +-- SAI to talk on waypoint reached +DELETE FROM `smart_scripts` WHERE (`entryorguid`= 29196 AND `source_type`= 0) OR (`entryorguid`= 2919600 AND `source_type`= 9); +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(29196, 0, 0, 0, 108, 0, 100, 0, 2, 1257120, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 2 Reached - Say Line 0'), +(29196, 0, 1, 0, 108, 0, 100, 0, 3, 1257120, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 3 Reached - Say Line 1'), +(29196, 0, 2, 0, 108, 0, 100, 0, 4, 1257120, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 4 Reached - Say Line 2'), +(29196, 0, 3, 0, 108, 0, 100, 0, 5, 1257120, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 5 Reached - Say Line 3'), +(29196, 0, 4, 0, 108, 0, 100, 0, 6, 1257120, 0, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 6 Reached - Say Line 4'), +(29196, 0, 5, 0, 108, 0, 100, 0, 7, 1257120, 0, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 7 Reached - Say Line 5'), +(29196, 0, 6, 0, 108, 0, 100, 0, 8, 1257120, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 8 Reached - Say Line 6'), +(29196, 0, 7, 0, 108, 0, 100, 0, 9, 1257120, 0, 0, 0, 0, 1, 7, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 9 Reached - Say Line 7'), +(29196, 0, 8, 0, 108, 0, 100, 0, 10, 1257120, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 10 Reached - Say Line 8'); From 4927c15f7ee5a1fcbad48b5b057d7f3b0fbe64ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:30:12 +0000 Subject: [PATCH 19/67] chore(DB): import pending files Referenced commit(s): bddaeeff944571e692e6a95beadab08494b8e430 --- .../rev_1784028882869393000.sql => db_world/2026_07_14_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784028882869393000.sql => db_world/2026_07_14_02.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1784028882869393000.sql b/data/sql/updates/db_world/2026_07_14_02.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1784028882869393000.sql rename to data/sql/updates/db_world/2026_07_14_02.sql index d67d1e5e4..d4a1ab402 100644 --- a/data/sql/updates/pending_db_world/rev_1784028882869393000.sql +++ b/data/sql/updates/db_world/2026_07_14_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_14_01 -> 2026_07_14_02 -- ------------------------------------------- -- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) -- Ebon Hold Re-haul Part 3: Lord Thorval (Walking Lecture) From e7823750f9e7cebebaaab816ec196a3ada73e49d Mon Sep 17 00:00:00 2001 From: Yehonal <147092+Yehonal@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:18:03 +0200 Subject: [PATCH 20/67] fix(Scripts/Commands): quote go creature name query safely (#26520) --- src/server/scripts/Commands/cs_go.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/scripts/Commands/cs_go.cpp b/src/server/scripts/Commands/cs_go.cpp index 03e657aa1..dd7d0dcbe 100644 --- a/src/server/scripts/Commands/cs_go.cpp +++ b/src/server/scripts/Commands/cs_go.cpp @@ -223,7 +223,7 @@ public: std::string str = name.data(); // Making subtractions to the last character does not with in string_view WorldDatabase.EscapeString(str); - QueryResult result = WorldDatabase.Query("SELECT entry FROM creature_template WHERE name = \"{}\" LIMIT 1", str); + QueryResult result = WorldDatabase.Query("SELECT entry FROM creature_template WHERE name = '{}' LIMIT 1", str); if (!result) { handler->SendErrorMessage(LANG_COMMAND_GOCREATNOTFOUND); From a43f01c3519cf940ee6d64caf5c9be766e6eaef2 Mon Sep 17 00:00:00 2001 From: Yehonal <147092+Yehonal@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:22:27 +0200 Subject: [PATCH 21/67] fix(Core/Instances): snapshot minions before state updates (#26519) --- src/server/game/Instances/InstanceScript.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/server/game/Instances/InstanceScript.cpp b/src/server/game/Instances/InstanceScript.cpp index f4dec4454..ab0813234 100644 --- a/src/server/game/Instances/InstanceScript.cpp +++ b/src/server/game/Instances/InstanceScript.cpp @@ -398,6 +398,8 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) if (id < bosses.size()) { BossInfo* bossInfo = &bosses[id]; + MinionSet minions = bossInfo->minion; + sScriptMgr->OnBeforeSetBossState(id, state, bossInfo->state, instance); if (bossInfo->state == TO_BE_DECIDED) // loading { @@ -410,8 +412,8 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) return false; if (state == DONE) - for (MinionSet::iterator i = bossInfo->minion.begin(); i != bossInfo->minion.end(); ++i) - if ((*i)->isWorldBoss() && (*i)->IsAlive()) + for (Creature* minion : minions) + if (minion && minion->isWorldBoss() && minion->IsAlive()) return false; bossInfo->state = state; @@ -422,8 +424,9 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) for (DoorSet::iterator i = bossInfo->door[type].begin(); i != bossInfo->door[type].end(); ++i) UpdateDoorState(*i); - for (MinionSet::iterator i = bossInfo->minion.begin(); i != bossInfo->minion.end(); ++i) - UpdateMinionState(*i, state); + for (Creature* minion : minions) + if (minion) + UpdateMinionState(minion, state); return true; } From 49cf6c14f3126f0415897b2ace7fe59659fce391 Mon Sep 17 00:00:00 2001 From: Yehonal <147092+Yehonal@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:43:38 +0200 Subject: [PATCH 22/67] fix(apps/startup-scripts): guard against duplicate/too-early server starts (#26149) Co-authored-by: YehonalBot <yehonalbot@users.noreply.github.com> --- apps/startup-scripts/src/starter | 172 +++++++++++++++++- .../test/test_startup_scripts.bats | 80 ++++++++ 2 files changed, 251 insertions(+), 1 deletion(-) diff --git a/apps/startup-scripts/src/starter b/apps/startup-scripts/src/starter index 4e58e8e7b..5585e7eba 100755 --- a/apps/startup-scripts/src/starter +++ b/apps/startup-scripts/src/starter @@ -45,6 +45,174 @@ if [ ! -f "$BINARY" ]; then exit 1 fi +function config_value() { + local key="$1" + + if [ -z "${CONFIG_ABS:-}" ] || [ ! -f "$CONFIG_ABS" ]; then + return 1 + fi + + awk -F= -v key="$key" ' + { + configKey = $1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", configKey) + } + configKey == key { + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/"/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + print value + exit + } + ' "$CONFIG_ABS" +} + +function command_contains_config() { + local command_line="$1" + + if [ -z "${CONFIG_ABS:-}" ]; then + return 0 + fi + + case "$command_line" in + *" -c $CONFIG_ABS"*|*" -c \"$CONFIG_ABS\""*) + return 0 + ;; + esac + + return 1 +} + +function find_existing_instances() { + local line pid command_line + + while IFS= read -r line; do + pid="${line%% *}" + command_line="${line#* }" + + [ -z "$pid" ] && continue + [ "$pid" = "$$" ] && continue + [ "$pid" = "$PPID" ] && continue + + case "$command_line" in + *"/starter "*|*"pgrep "*) + continue + ;; + esac + + if command_contains_config "$command_line"; then + printf '%s\n' "$line" + fi + done < <(pgrep -fa "$EXECPATH" 2>/dev/null || true) +} + +function listening_on_port() { + local port="$1" + + command -v ss >/dev/null 2>&1 || return 1 + ss -H -ltn "sport = :$port" 2>/dev/null | grep -q . +} + +function guarded_ports() { + local port soap_enabled soap_port + + if [ "$BINFILE" != "worldserver" ]; then + return 0 + fi + + port="$(config_value "WorldServerPort" || true)" + if [ -n "$port" ]; then + printf '%s\n' "$port" + fi + + soap_enabled="$(config_value "SOAP.Enabled" || true)" + soap_port="$(config_value "SOAP.Port" || true)" + if [ "$soap_enabled" = "1" ] && [ -n "$soap_port" ]; then + printf '%s\n' "$soap_port" + fi +} + +function find_listening_guard_ports() { + local port + + while IFS= read -r port; do + [ -z "$port" ] && continue + if listening_on_port "$port"; then + printf '%s\n' "$port" + fi + done < <(guarded_ports) +} + +function wait_for_startup_guard() { + local timeout="${AC_STARTUP_GUARD_TIMEOUT:-90}" + local interval="${AC_STARTUP_GUARD_INTERVAL:-2}" + local elapsed=0 + local instances ports + local sleep_for + local warned=0 + + if [ "${AC_STARTUP_SKIP_GUARD:-0}" = "1" ]; then + return 0 + fi + + case "$timeout" in + ''|*[!0-9]*) + echo "Startup guard: invalid AC_STARTUP_GUARD_TIMEOUT '$timeout', using 90." + timeout=90 + ;; + esac + + case "$interval" in + ''|*[!0-9]*) + echo "Startup guard: invalid AC_STARTUP_GUARD_INTERVAL '$interval', using 2." + interval=2 + ;; + esac + + if [ "$interval" -lt 1 ]; then + echo "Startup guard: AC_STARTUP_GUARD_INTERVAL must be at least 1, using 1." + interval=1 + fi + + while true; do + instances="$(find_existing_instances)" + ports="$(find_listening_guard_ports)" + + if [ -z "$instances" ] && [ -z "$ports" ]; then + return 0 + fi + + if [ "$elapsed" -ge "$timeout" ]; then + echo "Startup guard: refusing to start duplicate $BINFILE after waiting ${timeout}s." + if [ -n "$instances" ]; then + echo "Startup guard: existing matching process(es):" + echo "$instances" + fi + if [ -n "$ports" ]; then + echo "Startup guard: configured port(s) still listening: $(echo "$ports" | paste -sd ',' -)" + fi + exit 0 + fi + + if [ "$warned" -eq 0 ]; then + echo "Startup guard: waiting for existing $BINFILE instance or configured port(s) to clear..." + warned=1 + fi + + sleep_for="$interval" + if [ $((elapsed + sleep_for)) -gt "$timeout" ]; then + sleep_for=$((timeout - elapsed)) + fi + + [ "$sleep_for" -le 0 ] && continue + + sleep "$sleep_for" + elapsed=$((elapsed + sleep_for)) + done +} + # Create crashes directory if it doesn't exist mkdir -p "$CRASHES_PATH" @@ -55,6 +223,8 @@ cd "$BINPATH" || { EXECPATH=$(realpath "$BINFILE") +wait_for_startup_guard + if [ "$GDB_ENABLED" -eq 1 ]; then echo "Starting $EXECPATH with GDB enabled" @@ -148,4 +318,4 @@ else exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"} fi fi -fi \ No newline at end of file +fi diff --git a/apps/startup-scripts/test/test_startup_scripts.bats b/apps/startup-scripts/test/test_startup_scripts.bats index c7a90c7f6..82d49eba0 100755 --- a/apps/startup-scripts/test/test_startup_scripts.bats +++ b/apps/startup-scripts/test/test_startup_scripts.bats @@ -50,6 +50,86 @@ teardown() { [[ "$output" =~ "Test server starting" ]] } +@test "starter: should refuse duplicate binary/config start" { + cat > "$TEST_DIR/bin/duplicate-server" << 'EOF' +#!/usr/bin/env bash +sleep 30 +EOF + chmod +x "$TEST_DIR/bin/duplicate-server" + + "$TEST_DIR/bin/duplicate-server" -c "$TEST_DIR/test-server.conf" & + local duplicate_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "duplicate-server" "" "$TEST_DIR/test-server.conf" "" "" 0 + kill "$duplicate_pid" 2>/dev/null || true + wait "$duplicate_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "Startup guard: refusing to start duplicate duplicate-server" ]] + [[ "$output" =~ "existing matching process" ]] +} + +@test "starter: should refuse occupied configured worldserver ports" { + if ! command -v nc >/dev/null 2>&1; then + skip "nc is required for this test" + fi + + cat > "$TEST_DIR/bin/worldserver" << 'EOF' +#!/usr/bin/env bash +echo "worldserver should not start" +EOF + chmod +x "$TEST_DIR/bin/worldserver" + + local listen_port=40123 + cat > "$TEST_DIR/worldserver.conf" << EOF +WorldServerPort = $listen_port +SOAP.Enabled = 0 +EOF + nc -l 127.0.0.1 "$listen_port" >/dev/null & + local listener_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0 + kill "$listener_pid" 2>/dev/null || true + wait "$listener_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "Startup guard: refusing to start duplicate worldserver" ]] + [[ "$output" =~ "configured port(s) still listening: $listen_port" ]] +} + +@test "starter: should parse config keys literally for startup guard" { + if ! command -v nc >/dev/null 2>&1; then + skip "nc is required for this test" + fi + + cat > "$TEST_DIR/bin/worldserver" << 'EOF' +#!/usr/bin/env bash +echo "worldserver started" +EOF + chmod +x "$TEST_DIR/bin/worldserver" + + local listen_port=40124 + cat > "$TEST_DIR/worldserver.conf" << EOF +WorldServerPort = 40125 +SOAPXEnabled = 1 +SOAPXPort = $listen_port +SOAP.Enabled = 0 +EOF + nc -l 127.0.0.1 "$listen_port" >/dev/null & + local listener_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0 + kill "$listener_pid" 2>/dev/null || true + wait "$listener_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "worldserver started" ]] + [[ ! "$output" =~ "Startup guard: refusing" ]] +} + # ===== SIMPLE RESTARTER TESTS ===== @test "simple-restarter: should fail with missing parameters" { From 0a3e199df054ff410d28b85bc38575c9f07fc1c2 Mon Sep 17 00:00:00 2001 From: AlsoNotMehh <itsbrayanrodriguez@gmail.com> Date: Tue, 14 Jul 2026 17:45:05 -0400 Subject: [PATCH 23/67] fix(Core/GameObject): allow ritual helpers on cooldown (#26144) --- src/server/game/Entities/GameObject/GameObject.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index 523144f2c..955bd32cb 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -1846,14 +1846,19 @@ void GameObject::Use(Unit* user) if (GetUniqueUseCount() == info->summoningRitual.reqParticipants) return; + SpellCastResult castResult; if (info->summoningRitual.animSpell) - player->CastSpell(player, info->summoningRitual.animSpell, true); + castResult = player->CastSpell(player, info->summoningRitual.animSpell, true); else - player->CastSpell(player, GetSpellId(), + castResult = player->CastSpell(player, GetSpellId(), TriggerCastFlags(TRIGGERED_IGNORE_EFFECTS + | TRIGGERED_IGNORE_SPELL_AND_CATEGORY_CD | TRIGGERED_IGNORE_POWER_AND_REAGENT_COST | TRIGGERED_CAST_DIRECTLY)); + if (castResult != SPELL_CAST_OK) + return; + AddUniqueUse(player); // full amount unique participants including original summoner From de866ed4f80291be1169fea5498b7250353fb3db Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:54:41 -0400 Subject: [PATCH 24/67] perf(Core/Unit): Replace red-black tree with flat_multimap for aura containers (#25885) Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> Co-authored-by: Takenbacon <Takenbacon@users.noreply.github.com> --- src/server/game/Entities/Unit/Unit.cpp | 33 +- src/server/game/Entities/Unit/Unit.h | 9 +- .../Entities/FlatMultimapAuraPatternTest.cpp | 611 ++++++++++++++++++ 3 files changed, 634 insertions(+), 19 deletions(-) create mode 100644 src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 535417af1..9b08f1c0a 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -384,8 +384,6 @@ Unit::Unit() : WorldObject(), for (uint8 i = 0; i < MAX_GAMEOBJECT_SLOT; ++i) m_ObjectSlot[i].Clear(); - m_auraUpdateIterator = m_ownedAuras.end(); - m_interruptMask = 0; m_transform = 0; m_canModifyStats = false; @@ -4034,13 +4032,16 @@ void Unit::_UpdateSpells(uint32 time) } } - // m_auraUpdateIterator can be updated in indirect called code at aura remove to skip next planned to update but removed auras - for (m_auraUpdateIterator = m_ownedAuras.begin(); m_auraUpdateIterator != m_ownedAuras.end();) - { - Aura* i_aura = m_auraUpdateIterator->second; - ++m_auraUpdateIterator; // need shift to next for allow update if need into aura update - i_aura->UpdateOwner(time, this); - } + // snapshot - UpdateOwner can mutate the map; pointers stay valid (removed auras + // are deleted later in _DeleteRemovedAuras) + m_auraUpdateSnapshot.clear(); + m_auraUpdateSnapshot.reserve(m_ownedAuras.size()); + for (auto& [id, aura] : m_ownedAuras) + m_auraUpdateSnapshot.push_back(aura); + + for (Aura* aura : m_auraUpdateSnapshot) + if (!aura->IsRemoved()) + aura->UpdateOwner(time, this); // remove expired auras - do that after updates(used in scripts?) for (AuraMap::iterator i = m_ownedAuras.begin(); i != m_ownedAuras.end();) @@ -4986,10 +4987,6 @@ void Unit::RemoveOwnedAura(AuraMap::iterator& i, AuraRemoveMode removeMode) Aura* aura = i->second; ASSERT(!aura->IsRemoved()); - // if unit currently update aura list then make safe update iterator shift to next - if (m_auraUpdateIterator == i && m_auraUpdateIterator != m_ownedAuras.end()) - ++m_auraUpdateIterator; - m_ownedAuras.erase(i); m_removedAuras.push_back(aura); @@ -5063,6 +5060,9 @@ void Unit::RemoveAura(AuraApplicationMap::iterator& i, AuraRemoveMode mode) // Remove aura - for Area and Target auras if (aura->GetOwner() == this) aura->Remove(mode); + + // Aura::Remove can cascade into m_appliedAuras mutations - reset again for the caller + i = m_appliedAuras.begin(); } void Unit::RemoveAura(uint32 spellId, ObjectGuid caster, uint8 reqEffMask, AuraRemoveMode removeMode) @@ -5150,7 +5150,8 @@ void Unit::RemoveAppliedAuras(std::function<bool(AuraApplication const*)> const& { for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) { - if (check(iter->second)) + // RemoveAura no-ops on applications already mid-removal + if (!iter->second->GetRemoveMode() && check(iter->second)) { RemoveAura(iter); continue; @@ -5176,9 +5177,11 @@ void Unit::RemoveAppliedAuras(uint32 spellId, std::function<bool(AuraApplication { for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) { - if (check(iter->second)) + // RemoveAura no-ops on applications already mid-removal + if (!iter->second->GetRemoveMode() && check(iter->second)) { RemoveAura(iter); + iter = m_appliedAuras.lower_bound(spellId); continue; } ++iter; diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 13654929d..211482078 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -31,6 +31,7 @@ #include "ThreatManager.h" #include "UnitDefines.h" #include "UnitUtils.h" +#include <boost/container/flat_map.hpp> #include <functional> #include <utility> @@ -666,15 +667,15 @@ public: typedef std::unordered_set<Unit*> AttackerSet; typedef std::set<Unit*> ControlSet; - typedef std::multimap<uint32, Aura*> AuraMap; + typedef boost::container::flat_multimap<uint32, Aura*> AuraMap; typedef std::pair<AuraMap::const_iterator, AuraMap::const_iterator> AuraMapBounds; typedef std::pair<AuraMap::iterator, AuraMap::iterator> AuraMapBoundsNonConst; - typedef std::multimap<uint32, AuraApplication*> AuraApplicationMap; + typedef boost::container::flat_multimap<uint32, AuraApplication*> AuraApplicationMap; typedef std::pair<AuraApplicationMap::const_iterator, AuraApplicationMap::const_iterator> AuraApplicationMapBounds; typedef std::pair<AuraApplicationMap::iterator, AuraApplicationMap::iterator> AuraApplicationMapBoundsNonConst; - typedef std::multimap<AuraStateType, AuraApplication*> AuraStateAurasMap; + typedef boost::container::flat_multimap<AuraStateType, AuraApplication*> AuraStateAurasMap; typedef std::pair<AuraStateAurasMap::const_iterator, AuraStateAurasMap::const_iterator> AuraStateAurasMapBounds; typedef std::vector<AuraEffect*> AuraEffectList; @@ -2164,7 +2165,7 @@ protected: AuraMap m_ownedAuras; AuraApplicationMap m_appliedAuras; AuraList m_removedAuras; - AuraMap::iterator m_auraUpdateIterator; + std::vector<Aura*> m_auraUpdateSnapshot; // _UpdateSpells scratch buffer uint32 m_removedAurasCount; AuraEffectList m_modAuras[TOTAL_AURAS]; diff --git a/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp b/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp new file mode 100644 index 000000000..86c7208ff --- /dev/null +++ b/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp @@ -0,0 +1,611 @@ +/* + * 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 "gtest/gtest.h" +#include <boost/container/flat_map.hpp> +#include <cstdint> +#include <set> +#include <vector> + +// Lightweight fake aura — just enough state for pattern testing. +struct FakeAura +{ + uint32_t spellId; + bool removed = false; + bool expired = false; + bool updated = false; + + explicit FakeAura(uint32_t id) : spellId(id) {} + bool IsRemoved() const { return removed; } + bool IsExpired() const { return expired; } +}; + +using AuraMap = boost::container::flat_multimap<uint32_t, FakeAura*>; + +// ----------------------------------------------------------------------- +// 1. Snapshot-update pattern (mirrors _UpdateSpells snapshot loop) +// +// Snapshot pointers to a vector, then iterate the vector. +// Entries marked removed during iteration are skipped. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SnapshotUpdate) +{ + FakeAura a1(100), a2(100), a3(200), a4(300); + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(200, &a3); + map.emplace(300, &a4); + + // Snapshot pointers (mirrors Unit::_UpdateSpells) + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + ASSERT_EQ(snapshot.size(), 4u); + + // Mark a2 as removed mid-iteration (simulates cascading removal) + a2.removed = true; + + // Process snapshot — skip removed entries + for (FakeAura* aura : snapshot) + { + if (!aura->IsRemoved()) + aura->updated = true; + } + + EXPECT_TRUE(a1.updated); + EXPECT_FALSE(a2.updated); // skipped — was removed + EXPECT_TRUE(a3.updated); + EXPECT_TRUE(a4.updated); +} + +// ----------------------------------------------------------------------- +// 2. Erase-and-reset-to-begin (mirrors RemoveOwnedAura / expire loop) +// +// for (auto i = map.begin(); i != map.end();) +// if (should_remove) { erase(i); i = map.begin(); } +// else ++i; +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseResetToBegin) +{ + FakeAura a1(100), a2(200), a3(300), a4(400), a5(500); + a2.expired = true; + a4.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + map.emplace(500, &a5); + + std::vector<FakeAura*> removed; + + // Mirrors the expire loop in _UpdateSpells / RemoveOwnedAuras + for (AuraMap::iterator i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + FakeAura* aura = i->second; + map.erase(i); + i = map.begin(); // reset — flat_multimap invalidates all + removed.push_back(aura); + } + else + ++i; + } + + // a2 and a4 should have been removed + ASSERT_EQ(removed.size(), 2u); + EXPECT_EQ(removed[0]->spellId, 200u); + EXPECT_EQ(removed[1]->spellId, 400u); + + // Remaining entries + ASSERT_EQ(map.size(), 3u); + std::set<uint32_t> remaining; + for (auto& [id, aura] : map) + remaining.insert(id); + EXPECT_TRUE(remaining.count(100)); + EXPECT_TRUE(remaining.count(300)); + EXPECT_TRUE(remaining.count(500)); +} + +// ----------------------------------------------------------------------- +// 3. lower_bound / upper_bound erase (mirrors RemoveOwnedAura by spellId +// and RemoveAurasDueToSpell) +// +// Iterate a key range, erase matching entries, reset iterator to +// lower_bound after each erase. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, LowerUpperBoundErase) +{ + FakeAura a1(100), a2(100), a3(100), a4(200), a5(200); + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(200, &a4); + map.emplace(200, &a5); + + // Remove all entries with key 100 — mirrors RemoveOwnedAura(spellId) + uint32_t targetKey = 100; + for (auto itr = map.lower_bound(targetKey); + itr != map.upper_bound(targetKey);) + { + map.erase(itr); + itr = map.lower_bound(targetKey); // reset after erase + } + + // All key-100 entries gone + EXPECT_EQ(map.count(100), 0u); + + // Key-200 entries untouched + EXPECT_EQ(map.count(200), 2u); + ASSERT_EQ(map.size(), 2u); +} + +// ----------------------------------------------------------------------- +// 3b. Selective erase within a key range +// (mirrors RemoveOwnedAura with casterGUID / effectMask filter) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SelectiveLowerBoundErase) +{ + FakeAura a1(100), a2(100), a3(100); + // Only remove entries whose spellId matches AND which are expired + a1.expired = false; + a2.expired = true; + a3.expired = false; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + + uint32_t targetKey = 100; + for (auto itr = map.lower_bound(targetKey); + itr != map.upper_bound(targetKey);) + { + if (itr->second->IsExpired()) + { + map.erase(itr); + itr = map.lower_bound(targetKey); + } + else + ++itr; + } + + // Only a2 removed + EXPECT_EQ(map.count(100), 2u); + + // Verify the right ones survived + std::set<FakeAura*> survivors; + for (auto itr = map.lower_bound(100); itr != map.upper_bound(100); ++itr) + survivors.insert(itr->second); + EXPECT_TRUE(survivors.count(&a1)); + EXPECT_FALSE(survivors.count(&a2)); + EXPECT_TRUE(survivors.count(&a3)); +} + +// ----------------------------------------------------------------------- +// 4. Erase during snapshot (mirrors cascading removal — snapshot loop +// processes pointers while the map is mutated underneath) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseDuringSnapshot) +{ + FakeAura a1(100), a2(200), a3(300), a4(400); + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + + // Snapshot pointers + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + // Process snapshot; during processing, erase entries from the map + std::vector<uint32_t> processed; + for (FakeAura* aura : snapshot) + { + if (aura->IsRemoved()) + continue; + + processed.push_back(aura->spellId); + + // Simulate cascading removal: processing a1 causes a3 to be + // removed from the map and marked removed + if (aura == &a1) + { + a3.removed = true; + // Erase a3 from map by finding it + for (auto it = map.begin(); it != map.end(); ++it) + { + if (it->second == &a3) + { + map.erase(it); + break; + } + } + } + } + + // a1, a2, a4 processed; a3 skipped due to IsRemoved() + ASSERT_EQ(processed.size(), 3u); + EXPECT_EQ(processed[0], 100u); + EXPECT_EQ(processed[1], 200u); + EXPECT_EQ(processed[2], 400u); + + // Map should have 3 entries (a3 was erased) + EXPECT_EQ(map.size(), 3u); +} + +// ----------------------------------------------------------------------- +// 5. Insert during snapshot iteration +// Snapshot loop must NOT see newly inserted entries. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, InsertDuringSnapshot) +{ + FakeAura a1(100), a2(200); + FakeAura a3(300); // will be inserted during snapshot iteration + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + + // Snapshot before iteration + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + ASSERT_EQ(snapshot.size(), 2u); + + // During iteration, insert a new entry + std::vector<uint32_t> processed; + for (FakeAura* aura : snapshot) + { + if (!aura->IsRemoved()) + { + processed.push_back(aura->spellId); + aura->updated = true; + } + + // Insert a3 while iterating the snapshot + if (aura == &a1) + map.emplace(300, &a3); + } + + // Only the original 2 were processed + ASSERT_EQ(processed.size(), 2u); + EXPECT_EQ(processed[0], 100u); + EXPECT_EQ(processed[1], 200u); + + // a3 was NOT processed (not in snapshot) + EXPECT_FALSE(a3.updated); + + // But it IS in the map for future iterations + EXPECT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(300), 1u); +} + +// ----------------------------------------------------------------------- +// 6. Predicate-based removal with reset-to-begin +// (mirrors RemoveOwnedAuras(std::function<bool(Aura const*)>)) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, PredicateRemovalResetBegin) +{ + FakeAura a1(100), a2(200), a3(300), a4(400), a5(500); + // Remove even-numbered spell IDs + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + map.emplace(500, &a5); + + auto shouldRemove = [](FakeAura const* a) { + return (a->spellId % 200) == 0; + }; + + for (AuraMap::iterator iter = map.begin(); iter != map.end();) + { + if (shouldRemove(iter->second)) + { + map.erase(iter); + iter = map.begin(); // reset — mirrors RemoveOwnedAuras + continue; + } + ++iter; + } + + ASSERT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(100), 1u); + EXPECT_EQ(map.count(200), 0u); + EXPECT_EQ(map.count(300), 1u); + EXPECT_EQ(map.count(400), 0u); + EXPECT_EQ(map.count(500), 1u); +} + +// ----------------------------------------------------------------------- +// 7. Predicate removal within a key range with reset-to-lower_bound +// (mirrors RemoveOwnedAuras(spellId, check)) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, PredicateRemovalInKeyRange) +{ + FakeAura a1(100), a2(100), a3(100), a4(100); + a1.expired = false; + a2.expired = true; + a3.expired = false; + a4.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(100, &a4); + map.emplace(200, new FakeAura(200)); // different key, untouched + + uint32_t spellId = 100; + for (auto iter = map.lower_bound(spellId); + iter != map.upper_bound(spellId);) + { + if (iter->second->IsExpired()) + { + map.erase(iter); + iter = map.lower_bound(spellId); + continue; + } + ++iter; + } + + // 2 expired removed, 2 non-expired remain under key 100 + EXPECT_EQ(map.count(100), 2u); + // Key 200 untouched + EXPECT_EQ(map.count(200), 1u); + + // Clean up heap-allocated entry + for (auto& [id, aura] : map) + if (id == 200) + delete aura; +} + +// ----------------------------------------------------------------------- +// 8. AuraStateAuras erase-and-reset-to-lower_bound +// (mirrors _UnapplyAura's m_auraStateAuras cleanup) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, AuraStateMapErasePattern) +{ + // Uses an enum-like key (AuraStateType is an enum in the real code) + using AuraStateMap = boost::container::flat_multimap<uint32_t, FakeAura*>; + + FakeAura a1(10), a2(20), a3(30), a4(40); + AuraStateMap map; + // State 1 has multiple entries + map.emplace(1, &a1); + map.emplace(1, &a2); + map.emplace(1, &a3); + // State 2 has one entry + map.emplace(2, &a4); + + // Remove a2 from state 1 — mirrors _UnapplyAura pattern + uint32_t auraState = 1; + FakeAura* target = &a2; + for (auto itr = map.lower_bound(auraState); + itr != map.upper_bound(auraState);) + { + if (itr->second == target) + { + map.erase(itr); + itr = map.lower_bound(auraState); + continue; + } + ++itr; + } + + EXPECT_EQ(map.count(1), 2u); + EXPECT_EQ(map.count(2), 1u); + + // Verify a2 is gone, a1 and a3 remain + std::set<FakeAura*> stateOnes; + for (auto itr = map.lower_bound(1); itr != map.upper_bound(1); ++itr) + stateOnes.insert(itr->second); + EXPECT_TRUE(stateOnes.count(&a1)); + EXPECT_FALSE(stateOnes.count(&a2)); + EXPECT_TRUE(stateOnes.count(&a3)); +} + +// ----------------------------------------------------------------------- +// 9. Empty map edge cases — all patterns should be no-ops on empty maps +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EmptyMapPatterns) +{ + AuraMap map; + + // Snapshot of empty map + std::vector<FakeAura*> snapshot; + for (auto& [id, aura] : map) + snapshot.push_back(aura); + EXPECT_TRUE(snapshot.empty()); + + // Erase-reset-to-begin on empty map + for (auto i = map.begin(); i != map.end();) + { + map.erase(i); + i = map.begin(); + } + EXPECT_TRUE(map.empty()); + + // lower_bound/upper_bound on empty map + for (auto itr = map.lower_bound(100); itr != map.upper_bound(100);) + { + map.erase(itr); + itr = map.lower_bound(100); + } + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 10. Single-element map — boundary case for erase patterns +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SingleElementPatterns) +{ + FakeAura a1(100); + a1.expired = true; + + AuraMap map; + map.emplace(100, &a1); + + // Erase-reset-to-begin with single element + for (auto i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + map.erase(i); + i = map.begin(); + } + else + ++i; + } + + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 11. Erase all via reset-to-begin (stress test) +// Every entry matches removal criteria. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseAllViaResetBegin) +{ + constexpr int N = 50; + std::vector<FakeAura> auras; + auras.reserve(N); + + AuraMap map; + for (int i = 0; i < N; ++i) + { + auras.emplace_back(static_cast<uint32_t>(i * 10)); + map.emplace(auras.back().spellId, &auras.back()); + } + + ASSERT_EQ(map.size(), static_cast<size_t>(N)); + + // Remove everything — every entry triggers erase + reset + for (auto i = map.begin(); i != map.end();) + { + map.erase(i); + i = map.begin(); + } + + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 12. Duplicate keys with interleaved erase (multiple auras per spellId) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, DuplicateKeysInterleavedErase) +{ + FakeAura a1(100), a2(100), a3(100), a4(200), a5(200); + a2.expired = true; + a5.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(200, &a4); + map.emplace(200, &a5); + + // Global erase-reset-to-begin for expired entries + for (auto i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + map.erase(i); + i = map.begin(); + } + else + ++i; + } + + EXPECT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(100), 2u); + EXPECT_EQ(map.count(200), 1u); +} + +// ----------------------------------------------------------------------- +// 13. Cascade-after-reset (Unit::RemoveAura contract): Aura::Remove can +// mutate the map after _UnapplyAura reset the iterator. The DISABLED_ +// test is deliberate UB - run it only under ASAN, where it reports a +// deterministic heap-use-after-free: +// ./unit_tests --gtest_also_run_disabled_tests --gtest_filter='*StaleIteratorAfterCascade*' +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, DISABLED_StaleIteratorAfterCascade_AsanOnly) +{ + FakeAura a1(100), a2(200), a3(300), cascade1(999), cascade2(998); + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.shrink_to_fit(); + + // caller loop holds an iterator + AuraMap::iterator iter = map.begin(); + + // RemoveAura: erase + reset, then cascades grow past capacity -> realloc + map.erase(iter); + iter = map.begin(); + map.emplace(999, &cascade1); + map.emplace(998, &cascade2); + + // resume with the pre-cascade iterator: heap-use-after-free under ASAN + EXPECT_NE(iter->second->spellId, 0u); +} + +// The fixed contract: reset after all cascade mutations (mirrors Unit::RemoveAura). +TEST(FlatMultimapAuraPattern, IteratorResetAfterCascade) +{ + FakeAura a1(100), a2(200), a3(300), cascade1(999), cascade2(998); + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.shrink_to_fit(); + + AuraMap::iterator iter = map.begin(); + + map.erase(iter); + map.emplace(999, &cascade1); + map.emplace(998, &cascade2); + iter = map.begin(); // reset AFTER the cascades + + std::set<uint32_t> seen; + for (; iter != map.end(); ++iter) + seen.insert(iter->second->spellId); + + EXPECT_EQ(seen.size(), 4u); + EXPECT_FALSE(seen.count(100)); // removed + EXPECT_TRUE(seen.count(200)); + EXPECT_TRUE(seen.count(300)); + EXPECT_TRUE(seen.count(999)); // cascade-applied + EXPECT_TRUE(seen.count(998)); +} From c38705154aeefed9546e142d6e0fbba73da2f97f Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:46:24 -0300 Subject: [PATCH 25/67] fix(Scripts/Ulduar): disable druid Clearcasting during the General Vezax encounter (#26575) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: sogladev <sogladev@gmail.com> --- .../Northrend/Ulduar/Ulduar/boss_general_vezax.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp index de0677dd4..da84260a2 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp @@ -47,6 +47,7 @@ enum VezaxSpellData SPELL_CORRUPTED_WISDOM = 64646, SPELL_SHAMANISTIC_RAGE = 30823, SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 = 31876, + SPELL_DRUID_CLEARCASTING = 16870, SPELL_SUMMON_SARONITE_VAPORS = 63081, NPC_SARONITE_VAPORS = 33488, @@ -404,7 +405,7 @@ class spell_aura_of_despair_aura : public AuraScript bool Validate(SpellInfo const* /*spellInfo*/) override { - return ValidateSpellInfo({ SPELL_AURA_OF_DESPAIR_2, SPELL_CORRUPTED_RAGE, SPELL_CORRUPTED_WISDOM }); + return ValidateSpellInfo({ SPELL_AURA_OF_DESPAIR_2, SPELL_CORRUPTED_RAGE, SPELL_CORRUPTED_WISDOM, SPELL_DRUID_CLEARCASTING }); } void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) @@ -416,6 +417,10 @@ class spell_aura_of_despair_aura : public AuraScript return; target->CastSpell(target, SPELL_AURA_OF_DESPAIR_2, true); + + // Patch 3.2.0: Omen of Clarity can no longer trigger Clearcasting during the encounter + target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, true); + if (target->HasSpell(SPELL_SHAMANISTIC_RAGE)) caster->CastSpell(target, SPELL_CORRUPTED_RAGE, true); else if (target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1) || target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 + 1) || target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 + 2)) @@ -430,6 +435,8 @@ class spell_aura_of_despair_aura : public AuraScript target->RemoveAurasDueToSpell(SPELL_AURA_OF_DESPAIR_2); target->RemoveAurasDueToSpell(SPELL_CORRUPTED_RAGE); target->RemoveAurasDueToSpell(SPELL_CORRUPTED_WISDOM); + + target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, false); } } From d0d970b8f63715c330a88e8c38be560b62c8a84b Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:53:14 -0300 Subject: [PATCH 26/67] feat(Core/DB/Scripts): add creature_text_options engine for data-driven talk conditions (#25188) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Ludwig <sudlud@users.noreply.github.com> --- .../creature_text_options.sql | 114 ++++++++++++++++++ .../Database/Implementation/WorldDatabase.cpp | 1 + .../Database/Implementation/WorldDatabase.h | 1 + .../game/Entities/Creature/Creature.cpp | 15 +++ src/server/game/Entities/Creature/Creature.h | 4 + src/server/game/Texts/CreatureTextMgr.cpp | 78 ++++++++++++ src/server/game/Texts/CreatureTextMgr.h | 13 ++ src/server/game/World/World.cpp | 3 + .../MoltenCore/boss_majordomo_executus.cpp | 3 +- .../MoltenCore/boss_ragnaros.cpp | 3 +- .../EasternKingdoms/Karazhan/boss_moroes.cpp | 12 +- .../SunwellPlateau/boss_brutallus.cpp | 3 +- .../SunwellPlateau/boss_felmyst.cpp | 3 +- .../SunwellPlateau/boss_kalecgos.cpp | 3 +- .../BattleForMountHyjal/boss_anetheron.cpp | 19 +-- .../BattleForMountHyjal/boss_azgalor.cpp | 21 +--- .../BattleForMountHyjal/boss_kazrogal.cpp | 16 +-- .../boss_rage_winterchill.cpp | 19 +-- .../AzjolNerub/AzjolNerub/boss_anubarak.cpp | 9 +- .../boss_krikthir_the_gatewatcher.cpp | 12 +- .../boss_baltharus_the_warborn.cpp | 7 +- .../RubySanctum/boss_general_zarithrian.cpp | 7 +- .../RubySanctum/boss_halion.cpp | 13 +- .../RubySanctum/boss_saviana_ragefire.cpp | 9 +- .../Northrend/DraktharonKeep/boss_novos.cpp | 8 +- .../DraktharonKeep/boss_tharon_ja.cpp | 9 +- .../DraktharonKeep/boss_trollgore.cpp | 9 +- .../Northrend/Gundrak/boss_gal_darah.cpp | 11 +- .../Northrend/Gundrak/boss_moorabi.cpp | 7 +- .../Northrend/Gundrak/boss_slad_ran.cpp | 10 +- .../IcecrownCitadel/boss_the_lich_king.cpp | 9 +- .../Northrend/Naxxramas/boss_razuvious.cpp | 3 +- .../Nexus/Nexus/boss_keristrasza.cpp | 9 +- .../Nexus/Nexus/boss_magus_telestra.cpp | 9 +- .../Northrend/Nexus/Nexus/boss_ormorok.cpp | 9 +- .../Outland/BlackTemple/boss_bloodboil.cpp | 13 +- .../Outland/BlackTemple/boss_illidan.cpp | 14 +-- .../BlackTemple/boss_illidari_council.cpp | 7 +- .../BlackTemple/boss_mother_shahraz.cpp | 15 +-- .../BlackTemple/boss_reliquary_of_souls.cpp | 39 +----- .../BlackTemple/boss_teron_gorefiend.cpp | 15 +-- .../BlackTemple/boss_warlord_najentus.cpp | 17 +-- .../boss_fathomlord_karathress.cpp | 13 +- .../boss_hydross_the_unstable.cpp | 15 +-- .../SerpentShrine/boss_lady_vashj.cpp | 12 +- .../boss_leotheras_the_blind.cpp | 3 - .../boss_morogrim_tidewalker.cpp | 16 +-- .../scripts/Outland/GruulsLair/boss_gruul.cpp | 15 +-- .../GruulsLair/boss_high_king_maulgar.cpp | 16 +-- .../MagtheridonsLair/boss_magtheridon.cpp | 13 +- .../TempestKeep/Eye/boss_astromancer.cpp | 5 +- .../TempestKeep/Eye/boss_void_reaver.cpp | 13 +- 52 files changed, 305 insertions(+), 417 deletions(-) create mode 100644 data/sql/updates/pending_db_world/creature_text_options.sql diff --git a/data/sql/updates/pending_db_world/creature_text_options.sql b/data/sql/updates/pending_db_world/creature_text_options.sql new file mode 100644 index 000000000..efe07af1c --- /dev/null +++ b/data/sql/updates/pending_db_world/creature_text_options.sql @@ -0,0 +1,114 @@ +-- Reusable option sets for creature text groups +CREATE TABLE IF NOT EXISTS `creature_text_option_sets` ( + `SetID` TINYINT UNSIGNED NOT NULL, + `Cooldown` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Group cooldown in ms before it can fire again', + `TriggerChance` TINYINT UNSIGNED NOT NULL DEFAULT 100 COMMENT '0-100 pct chance to fire at all', + `PlayerOnly` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Only fire if target is a player', + `comment` VARCHAR(255) DEFAULT '', + PRIMARY KEY (`SetID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DELETE FROM `creature_text_option_sets`; +INSERT INTO `creature_text_option_sets` (`SetID`, `Cooldown`, `TriggerChance`, `PlayerOnly`, `comment`) VALUES +(1, 6000, 100, 0, 'Standard kill yell: 6s cooldown'), +(2, 6000, 100, 1, 'Player-only kill yell: 6s cooldown'), +(3, 5000, 100, 1, 'Short cooldown player-only kill yell: 5s cooldown'), +(4, 0, 30, 0, 'Chance-based: 30% trigger, no cooldown'), +(5, 5000, 100, 0, 'Standard kill yell: 5s cooldown'), +(6, 0, 25, 1, 'Chance-based: 25% trigger, player-only'), +(7, 0, 50, 1, 'Chance-based: 50% trigger, player-only'); + +-- Assign option sets to creature text groups +CREATE TABLE IF NOT EXISTS `creature_text_options` ( + `CreatureID` INT UNSIGNED NOT NULL, + `GroupID` TINYINT UNSIGNED NOT NULL, + `OptionSetID` TINYINT UNSIGNED NOT NULL, + PRIMARY KEY (`CreatureID`, `GroupID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DELETE FROM `creature_text_options`; +INSERT INTO `creature_text_options` (`CreatureID`, `GroupID`, `OptionSetID`) VALUES +-- Black Temple +(22887, 2, 3), -- High Warlord Najentus SAY_SLAY +(22871, 2, 2), -- Teron Gorefiend SAY_SLAY +(22917, 1, 1), -- Illidan Stormrage SAY_ILLIDAN_KILL +(22947, 3, 1), -- Mother Shahraz SAY_SLAY +(22949, 3, 1), -- Gathios the Shatterer SAY_COUNCIL_SLAY +(22950, 3, 1), -- High Nethermancer Zerevor SAY_COUNCIL_SLAY +(22951, 3, 1), -- Lady Malande SAY_COUNCIL_SLAY +(22952, 3, 1), -- Veras Darkshadow SAY_COUNCIL_SLAY + +-- Azjol-Nerub +(28684, 1, 1), -- Krik'thir the Gatewatcher SAY_SLAY +(29120, 1, 1), -- Anub'arak SAY_SLAY + +-- Drak'Tharon Keep +(26631, 1, 1), -- Novos the Summoner SAY_KILL +(26632, 1, 1), -- The Prophet Tharon'ja SAY_KILL +(26630, 1, 1), -- Trollgore SAY_KILL + +-- Gundrak +(29306, 1, 1), -- Gal'darah SAY_SLAY +(29305, 1, 1), -- Moorabi SAY_SLAY +(29304, 1, 1), -- Slad'ran SAY_SLAY + +-- The Nexus +(26723, 1, 1), -- Keristrasza SAY_SLAY +(26731, 1, 1), -- Grand Magus Telestra SAY_KILL +(26794, 5, 1), -- Ormorok the Tree-Shaper SAY_KILL + +-- Ruby Sanctum +(39863, 6, 2), -- Halion SAY_KILL +(39751, 2, 1), -- Baltharus the Warborn SAY_KILL +(39746, 1, 1), -- General Zarithrian SAY_KILL +(39747, 3, 1), -- Saviana Ragefire SAY_KILL + +-- Icecrown Citadel +(36597, 10, 3), -- The Lich King SAY_LK_KILL + +-- Naxxramas +(16061, 1, 4), -- Instructor Razuvious SAY_SLAY + +-- Karazhan +(15687, 2, 3), -- Moroes SAY_KILL + +-- Battle for Mount Hyjal +(17888, 0, 2), -- Kazrogal SAY_ONSLAY +(17842, 1, 2), -- Azgalor SAY_ONSLAY +(17767, 1, 2), -- Rage Winterchill SAY_ONSLAY +(17808, 1, 2), -- Anetheron SAY_ONSLAY + +-- Serpentshrine Cavern +(21212, 6, 1), -- Lady Vashj SAY_SLAY +(21216, 2, 1), -- Hydross the Unstable SAY_CLEAN_SLAY +(21216, 5, 1), -- Hydross the Unstable SAY_CORRUPT_SLAY +(21214, 5, 1), -- Fathom-Lord Karathress SAY_SLAY +(21213, 3, 1), -- Morogrim Tidewalker SAY_SLAY + +-- Gruul's Lair +(19044, 3, 5), -- Gruul the Dragonkiller SAY_SLAY +(18831, 3, 5), -- High King Maulgar SAY_SLAY + +-- Magtheridon's Lair +(17257, 2, 5), -- Magtheridon SAY_SLAY + +-- Tempest Keep: The Eye +(19516, 1, 5), -- Void Reaver SAY_SLAY + +-- Black Temple (additional) +(22948, 1, 1), -- Gurtogg Bloodboil SAY_SLAY +(23418, 2, 1), -- Essence of Suffering SUFF_SAY_SLAY +(23419, 1, 1), -- Essence of Desire DESI_SAY_SLAY +(23420, 1, 1), -- Essence of Anger ANGER_SAY_SLAY + +-- Molten Core +(12018, 2, 6), -- Majordomo Executus SAY_SLAY +(11502, 9, 6), -- Ragnaros SAY_KILL + +-- Sunwell Plateau +(24882, 6, 7), -- Brutallus YELL_KILL +(25038, 1, 7), -- Felmyst YELL_KILL +(24850, 1, 7), -- Kalecgos SAY_EVIL_SLAY + +-- Tempest Keep: The Eye (additional) +(18805, 2, 7); -- High Astromancer Solarian SAY_KILL diff --git a/src/server/database/Database/Implementation/WorldDatabase.cpp b/src/server/database/Database/Implementation/WorldDatabase.cpp index dfe7b047d..11b8a613b 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.cpp +++ b/src/server/database/Database/Implementation/WorldDatabase.cpp @@ -27,6 +27,7 @@ void WorldDatabaseConnection::DoPrepareStatements() PrepareStatement(WORLD_DEL_CRELINKED_RESPAWN, "DELETE FROM linked_respawn WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(WORLD_REP_CREATURE_LINKED_RESPAWN, "REPLACE INTO linked_respawn (guid, linkedGuid) VALUES (?, ?)", CONNECTION_ASYNC); PrepareStatement(WORLD_SEL_CREATURE_TEXT, "SELECT CreatureID, GroupID, ID, Text, Type, Language, Probability, Emote, Duration, Sound, BroadcastTextId, TextRange FROM creature_text", CONNECTION_SYNCH); + PrepareStatement(WORLD_SEL_CREATURE_TEXT_OPTIONS, "SELECT o.CreatureID, o.GroupID, s.Cooldown, s.TriggerChance, s.PlayerOnly FROM creature_text_options o JOIN creature_text_option_sets s ON o.OptionSetID = s.SetID", CONNECTION_SYNCH); PrepareStatement(WORLD_SEL_SMART_SCRIPTS, "SELECT entryorguid, source_type, id, link, event_type, event_phase_mask, event_chance, event_flags, event_param1, event_param2, event_param3, event_param4, event_param5, event_param6, action_type, action_param1, action_param2, action_param3, action_param4, action_param5, action_param6, target_type, target_param1, target_param2, target_param3, target_param4, target_x, target_y, target_z, target_o FROM smart_scripts ORDER BY entryorguid, source_type, id, link", CONNECTION_SYNCH); PrepareStatement(WORLD_SEL_SMARTAI_WP, "SELECT entry, pointid, position_x, position_y, position_z, orientation, delay FROM waypoints ORDER BY entry, pointid", CONNECTION_SYNCH); PrepareStatement(WORLD_DEL_GAMEOBJECT, "DELETE FROM gameobject WHERE guid = ?", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/WorldDatabase.h b/src/server/database/Database/Implementation/WorldDatabase.h index 6874a81d4..c82fd1652 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.h +++ b/src/server/database/Database/Implementation/WorldDatabase.h @@ -32,6 +32,7 @@ enum WorldDatabaseStatements : uint32 WORLD_DEL_CRELINKED_RESPAWN, WORLD_REP_CREATURE_LINKED_RESPAWN, WORLD_SEL_CREATURE_TEXT, + WORLD_SEL_CREATURE_TEXT_OPTIONS, WORLD_SEL_SMART_SCRIPTS, WORLD_SEL_SMARTAI_WP, WORLD_DEL_GAMEOBJECT, diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 60acd2781..58c4336e8 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -3817,6 +3817,21 @@ void Creature::ClearTextRepeatGroup(uint8 textGroup) groupItr->second.clear(); } +bool Creature::IsTextOnCooldown(uint8 textGroup) const +{ + auto itr = m_textCooldowns.find(textGroup); + if (itr == m_textCooldowns.end()) + return false; + + return GameTime::GetGameTime().count() < itr->second; +} + +void Creature::SetTextCooldown(uint8 textGroup, uint32 cooldownMs) +{ + m_textCooldowns[textGroup] = + GameTime::GetGameTime().count() + ((cooldownMs + 999) / 1000); +} + void Creature::SetRespawnTime(uint32 respawn) { m_respawnTime = respawn ? GameTime::GetGameTime().count() + respawn : 0; diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index b492bd6c8..6b14d82f2 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -411,6 +411,9 @@ public: void SetTextRepeatId(uint8 textGroup, uint8 id); void ClearTextRepeatGroup(uint8 textGroup); + bool IsTextOnCooldown(uint8 textGroup) const; + void SetTextCooldown(uint8 textGroup, uint32 cooldownMs); + bool IsFreeToMove(); static constexpr uint32 MOVE_CIRCLE_CHECK_INTERVAL = 3000; static constexpr uint32 MOVE_BACKWARDS_CHECK_INTERVAL = 2000; @@ -548,6 +551,7 @@ private: ObjectGuid _spellFocusTarget; ///> Saved target during spell focus for restoration CreatureTextRepeatGroup m_textRepeat; + std::unordered_map<uint8, uint32> m_textCooldowns; // groupID -> expiry time (s) bool _isMissingSwimmingFlagOutOfCombat; diff --git a/src/server/game/Texts/CreatureTextMgr.cpp b/src/server/game/Texts/CreatureTextMgr.cpp index 501bea861..7ecb8153f 100644 --- a/src/server/game/Texts/CreatureTextMgr.cpp +++ b/src/server/game/Texts/CreatureTextMgr.cpp @@ -21,9 +21,11 @@ #include "Chat.h" #include "Common.h" #include "DatabaseEnv.h" +#include "GameTime.h" #include "GridNotifiers.h" #include "MiscPackets.h" #include "ObjectMgr.h" +#include "Random.h" class CreatureTextBuilder { @@ -196,6 +198,62 @@ void CreatureTextMgr::LoadCreatureTextLocales() LOG_INFO("server.loading", " "); } +void CreatureTextMgr::LoadCreatureTextOptions() +{ + uint32 oldMSTime = getMSTime(); + + mTextOptionsMap.clear(); + + WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_CREATURE_TEXT_OPTIONS); + PreparedQueryResult result = WorldDatabase.Query(stmt); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 creature text options. DB table `creature_text_options` is empty."); + LOG_INFO("server.loading", " "); + return; + } + + uint32 count = 0; + + do + { + Field* fields = result->Fetch(); + uint32 creatureId = fields[0].Get<uint32>(); + uint8 groupId = fields[1].Get<uint8>(); + + CreatureTextOptions options; + options.cooldown = fields[2].Get<uint32>(); + options.triggerChance = fields[3].Get<uint8>(); + options.playerOnly = fields[4].Get<bool>(); + + if (options.triggerChance > 100) + { + LOG_ERROR("sql.sql", "CreatureTextMgr: Entry {}, Group {} in table `creature_text_options` has TriggerChance {} > 100. Setting to 100.", creatureId, groupId, options.triggerChance); + options.triggerChance = 100; + } + + mTextOptionsMap[creatureId][groupId] = options; + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} Creature Text Options in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +CreatureTextOptions const* CreatureTextMgr::GetTextOptions(uint32 entry, uint8 group) const +{ + auto entryItr = mTextOptionsMap.find(entry); + if (entryItr == mTextOptionsMap.end()) + return nullptr; + + auto groupItr = entryItr->second.find(group); + if (groupItr == entryItr->second.end()) + return nullptr; + + return &groupItr->second; +} + uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject const* target /*= nullptr*/, ChatMsg msgType /*= CHAT_MSG_ADDON*/, Language language /*= LANG_ADDON*/, CreatureTextRange range /*= TEXT_RANGE_NORMAL*/, uint32 sound /*= 0*/, TeamId teamId /*= TEAM_NEUTRAL*/, bool gmOnly /*= false*/, Player* srcPlr /*= nullptr*/) { if (!source) @@ -218,6 +276,21 @@ uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject } CreatureTextGroup const& textGroupContainer = itr->second; //has all texts in the group + + // Check creature_text_options for this group + CreatureTextOptions const* options = GetTextOptions(source->GetEntry(), textGroup); + if (options) + { + if (options->playerOnly && (!target || !target->IsPlayer())) + return 0; + + if (options->triggerChance < 100 && !roll_chance_i(options->triggerChance)) + return 0; + + if (options->cooldown > 0 && source->IsTextOnCooldown(textGroup)) + return 0; + } + CreatureTextRepeatIds repeatGroup = source->GetTextRepeatGroup(textGroup);//has all textIDs from the group that were already said CreatureTextGroup tempGroup;//will use this to talk after sorting repeatGroup @@ -265,6 +338,11 @@ uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject } source->SetTextRepeatId(textGroup, iter->id); + + // Set cooldown if creature_text_options specifies one + if (options && options->cooldown > 0) + source->SetTextCooldown(textGroup, options->cooldown); + return iter->duration; } diff --git a/src/server/game/Texts/CreatureTextMgr.h b/src/server/game/Texts/CreatureTextMgr.h index 37a393496..22ca7dbee 100644 --- a/src/server/game/Texts/CreatureTextMgr.h +++ b/src/server/game/Texts/CreatureTextMgr.h @@ -71,10 +71,20 @@ struct CreatureTextId uint32 textId; }; +struct CreatureTextOptions +{ + uint32 cooldown; // ms before this group can fire again + uint8 triggerChance; // 0-100 chance to fire at all + bool playerOnly; // only fire if target is a player +}; + typedef std::vector<CreatureTextEntry> CreatureTextGroup; // texts in a group typedef std::unordered_map<uint8, CreatureTextGroup> CreatureTextHolder; // groups for a creature by groupid typedef std::unordered_map<uint32, CreatureTextHolder> CreatureTextMap; // all creatures by entry +typedef std::unordered_map<uint8, CreatureTextOptions> CreatureTextOptionsMap; // options per group +typedef std::unordered_map<uint32, CreatureTextOptionsMap> CreatureTextOptionsContainer; // per creature entry + typedef std::map<CreatureTextId, CreatureTextLocale> LocaleCreatureTextMap; class CreatureTextMgr @@ -87,7 +97,9 @@ public: ~CreatureTextMgr() { } void LoadCreatureTexts(); void LoadCreatureTextLocales(); + void LoadCreatureTextOptions(); CreatureTextMap const& GetTextMap() const { return mTextMap; } + CreatureTextOptions const* GetTextOptions(uint32 entry, uint8 group) const; void SendSound(Creature* source, uint32 sound, ChatMsg msgType, WorldObject const* target, CreatureTextRange range, TeamId teamId, bool gmOnly); void SendEmote(Unit* source, uint32 emote); @@ -105,6 +117,7 @@ private: CreatureTextMap mTextMap; LocaleCreatureTextMap mLocaleTextMap; + CreatureTextOptionsContainer mTextOptionsMap; }; #define sCreatureTextMgr CreatureTextMgr::instance() diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index c416a6080..c12ff71b8 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -869,6 +869,9 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Creature Texts..."); sCreatureTextMgr->LoadCreatureTexts(); + LOG_INFO("server.loading", "Loading Creature Text Options..."); + sCreatureTextMgr->LoadCreatureTextOptions(); + LOG_INFO("server.loading", "Loading Creature Text Locales..."); sCreatureTextMgr->LoadCreatureTextLocales(); diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp index bb563de0a..ea9267357 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp @@ -219,8 +219,7 @@ struct boss_majordomo : public BossAI void KilledUnit(Unit* victim) override { - if (roll_chance_i(25) && victim->IsPlayer()) - Talk(SAY_SLAY); + Talk(SAY_SLAY, victim); } void JustEngagedWith(Unit* /*attacker*/) override diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp index 6538947e3..73eab62c6 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp @@ -209,8 +209,7 @@ struct boss_ragnaros : public BossAI void KilledUnit(Unit* victim) override { - if (roll_chance_i(25) && victim->IsPlayer()) - Talk(SAY_KILL); + Talk(SAY_KILL, victim); } void AttackStart(Unit* target) override diff --git a/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp b/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp index 168037e7a..97bd59505 100644 --- a/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp +++ b/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp @@ -117,7 +117,6 @@ struct boss_moroes : public BossAI { BossAI::Reset(); DoCastSelf(SPELL_DUAL_WIELD, true); - _recentlySpoken = false; _vanished = false; me->SetImmuneToAll(false); me->SetReactState(REACT_AGGRESSIVE); @@ -170,15 +169,7 @@ struct boss_moroes : public BossAI void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer()) - { - Talk(SAY_KILL); - _recentlySpoken = true; - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override @@ -243,7 +234,6 @@ struct boss_moroes : public BossAI private: EventMap _events2; uint8 _activeGuests; - bool _recentlySpoken; bool _vanished; }; diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp index 0e55c9d1d..5e8570136 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp @@ -104,8 +104,7 @@ struct boss_brutallus : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(YELL_KILL); + Talk(YELL_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp index 6aee91f41..d3ce0fa71 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp @@ -186,8 +186,7 @@ struct boss_felmyst : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(YELL_KILL); + Talk(YELL_KILL, victim); } void SpellHitTarget(Unit* target, const SpellInfo* spell) override diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp index c23569e4e..1249a90d7 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp @@ -260,8 +260,7 @@ struct boss_kalecgos : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(SAY_EVIL_SLAY); + Talk(SAY_EVIL_SLAY, victim); } private: diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp index 0ac933683..0e0e923ef 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp @@ -47,10 +47,7 @@ enum Texts struct boss_anetheron : public BossAI { public: - boss_anetheron(Creature* creature) : BossAI(creature, DATA_ANETHERON) - { - _recentlySpoken = false; - } + boss_anetheron(Creature* creature) : BossAI(creature, DATA_ANETHERON) { } void JustEngagedWith(Unit * who) override { @@ -114,16 +111,8 @@ public: void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -132,8 +121,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_anetheron_sleep : public SpellScript diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp index 22024068e..abbd9374d 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp @@ -44,10 +44,7 @@ enum Texts struct boss_azgalor : public BossAI { public: - boss_azgalor(Creature* creature) : BossAI(creature, DATA_AZGALOR) - { - _recentlySpoken = false; - } + boss_azgalor(Creature* creature) : BossAI(creature, DATA_AZGALOR) { } void JustEngagedWith(Unit * who) override { @@ -85,18 +82,10 @@ public: me->GetMotionMaster()->MoveWaypoint(HORDE_BOSS_PATH, false); } - void KilledUnit(Unit * victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -111,8 +100,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_azgalor_doom : public SpellScript diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp index 889ccc178..42cfb545d 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp @@ -52,7 +52,6 @@ public: void Reset() override { - _recentlySpoken = false; _markCounter = 0; BossAI::Reset(); } @@ -104,18 +103,10 @@ public: me->GetMotionMaster()->MoveWaypoint(HORDE_BOSS_PATH, false); } - void KilledUnit(Unit * victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -125,7 +116,6 @@ public: } private: - bool _recentlySpoken; uint8 _markCounter; }; diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp index 2bdf44abf..9e2a8a26c 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp @@ -47,10 +47,7 @@ enum Misc struct boss_rage_winterchill : public BossAI { public: - boss_rage_winterchill(Creature* creature) : BossAI(creature, DATA_WINTERCHILL) - { - _recentlySpoken = false; - } + boss_rage_winterchill(Creature* creature) : BossAI(creature, DATA_WINTERCHILL) { } void JustEngagedWith(Unit* who) override { @@ -117,16 +114,8 @@ public: void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit* killer) override @@ -135,8 +124,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; void AddSC_boss_rage_winterchill() diff --git a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp index 640bb9c98..4d06c331d 100644 --- a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp +++ b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp @@ -69,8 +69,7 @@ enum Events EVENT_SUMMON_GUARDIAN = 8, EVENT_SUMMON_VENOMANCER = 9, EVENT_SUMMON_DARTER = 10, - EVENT_SUMMON_ASSASSINS = 11, - EVENT_KILL_TALK = 12 + EVENT_SUMMON_ASSASSINS = 11 }; enum CreatureIds @@ -262,11 +261,7 @@ struct boss_anub_arak : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void SummonedCreatureDies(Creature* summon, Unit* /*killer*/) override diff --git a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp index be33fad5a..90e24370f 100644 --- a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp +++ b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp @@ -58,7 +58,6 @@ public: boss_krik_thirAI(Creature* creature) : BossAI(creature, DATA_KRIKTHIR) { _initTalk = false; - _canTalk = true; _minionInCombat = false; } @@ -79,7 +78,6 @@ public: }); }); - _canTalk = true; _minionInCombat = false; _firstCall = true; _minionsEngaged = 0; @@ -236,14 +234,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - _canTalk = false; - Talk(SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _canTalk = true; - }, 6s); - } + Talk(SAY_SLAY); } void SummonedCreatureDies(Creature* summon, Unit*) override @@ -253,7 +244,6 @@ public: private: bool _initTalk; - bool _canTalk; bool _minionInCombat; uint8 _minionsEngaged; bool _firstCall; diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp index 5a8cc49b1..802aced27 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp @@ -66,7 +66,6 @@ enum Events EVENT_CHECK_HEALTH1 = 4, EVENT_CHECK_HEALTH2 = 5, EVENT_CHECK_HEALTH3 = 6, - EVENT_KILL_TALK = 7, EVENT_SUMMON_CLONE = 8, EVENT_XERESTRASZA_EVENT_0 = 1, @@ -191,11 +190,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp index faf314e6f..fe0ea6de9 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp @@ -47,7 +47,6 @@ enum Misc EVENT_INTIDMDATING_ROAR = 2, EVENT_SUMMON_ADDS1 = 3, EVENT_SUMMON_ADDS2 = 4, - EVENT_KILL_TALK = 5, // Onyx Flamecaller EVENT_BLAST_NOVA = 6, @@ -136,11 +135,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp index 5a53b1bdd..d289520f6 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp @@ -131,7 +131,6 @@ enum Events EVENT_FIERY_COMBUSTION = 5, EVENT_TAIL_LASH = 6, EVENT_CHECK_HEALTH = 7, - EVENT_KILL_TALK = 8, EVENT_TRIGGER_BERSERK = 9, EVENT_HALION_VISIBILITY = 10, @@ -310,11 +309,7 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override @@ -477,11 +472,7 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !_events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - _events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp index 2bebd1502..7792dfeb1 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp @@ -46,8 +46,7 @@ enum Events EVENT_CONFLAGRATION = 4, EVENT_LAND_GROUND = 5, EVENT_AIR_MOVEMENT = 6, - EVENT_LAND_BACK = 7, - EVENT_KILL_TALK = 8 + EVENT_LAND_BACK = 7 }; enum Misc @@ -121,11 +120,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp index 36f8b555c..dd1842cd6 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp @@ -56,8 +56,6 @@ enum Misc NPC_CRYSTAL_HANDLER = 26627, NPC_SUMMON_CRYSTAL_HANDLER_TARGET = 27583, - EVENT_KILL_TALK = 1, - ROOM_RIGHT = 0, ROOM_LEFT = 1, ROOM_STAIRS = 2 @@ -206,11 +204,7 @@ struct boss_novos : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp index 1a240ce15..43fc5e74d 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp @@ -60,8 +60,7 @@ enum Misc EVENT_SPELL_LIGHTNING_BREATH = 5, EVENT_SPELL_POISON_CLOUD = 6, EVENT_TURN_FLESH_REAL = 9, - EVENT_TURN_BONES_REAL = 10, - EVENT_KILL_TALK = 11 + EVENT_TURN_BONES_REAL = 10 }; class boss_tharon_ja : public CreatureScript @@ -106,11 +105,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void DoAction(int32 param) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp index 8380f1caa..6d71ac5cd 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp @@ -55,8 +55,7 @@ enum Events EVENT_SPELL_CRUSH = 2, EVENT_SPELL_CONSUME = 3, EVENT_SPELL_CORPSE_EXPLODE = 4, - EVENT_SPAWN_INVADERS = 5, - EVENT_KILL_TALK = 6 + EVENT_SPAWN_INVADERS = 5 }; class boss_trollgore : public CreatureScript @@ -102,11 +101,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp b/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp index f44da0ccb..8f4af9474 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp @@ -47,11 +47,6 @@ enum Yells SAY_IMPALE = 5 }; -enum Events -{ - EVENT_KILL_TALK = 1 -}; - struct boss_gal_darah : public BossAI { boss_gal_darah(Creature* creature) : BossAI(creature, DATA_GAL_DARAH) { } @@ -160,11 +155,7 @@ struct boss_gal_darah : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } private: diff --git a/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp b/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp index 2d405fe5d..d369dc0f6 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp @@ -55,7 +55,6 @@ enum Events EVENT_NUMBLING_SHOUT = 2, EVENT_DETERMINED_STAB = 3, EVENT_TRANSFORMATION = 4, - EVENT_KILL_TALK = 5, EVENT_PHANTOM = 10 }; @@ -125,11 +124,7 @@ public: void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp b/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp index 2d8b6ad56..2d3eaf0b0 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp @@ -49,9 +49,7 @@ enum Misc MAX_VIPER = 2, MAX_CONSTRICTOR = 3, - MAX_SUMMONS = 5, - - EVENT_KILL_TALK = 1 + MAX_SUMMONS = 5 }; const Position SpawnLoc[] = @@ -137,11 +135,7 @@ public: void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } private: diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp index b27e9008b..05c6838d6 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp @@ -638,7 +638,6 @@ public: uint32 _necroticPlagueStack; uint32 _vileSpiritExplosions; uint16 _positionCheckTimer; - uint32 _lastTalkTimeKill; uint32 _lastTalkTimeBuff; bool _bFrostmournePhase; bool _bFordringMustFallYell; @@ -649,7 +648,6 @@ public: _necroticPlagueStack = 0; _vileSpiritExplosions = 0; _positionCheckTimer = 5000; - _lastTalkTimeKill = 0; _lastTalkTimeBuff = 0; _bFrostmournePhase = false; _bFordringMustFallYell = false; @@ -721,11 +719,8 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !me->IsInEvadeMode() && _phase != PHASE_OUTRO && _lastTalkTimeKill + 5 < GameTime::GetGameTime().count()) - { - _lastTalkTimeKill = GameTime::GetGameTime().count(); - Talk(SAY_LK_KILL); - } + if (!me->IsInEvadeMode() && _phase != PHASE_OUTRO) + Talk(SAY_LK_KILL, victim); } void DoAction(int32 action) override diff --git a/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp b/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp index d6fbd906a..f0e7cf6a6 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp @@ -205,8 +205,7 @@ public: void KilledUnit(Unit* who) override { - if (roll_chance_i(30)) - Talk(SAY_SLAY); + Talk(SAY_SLAY); if (who->IsPlayer()) instance->StorePersistentData(PERSISTENT_DATA_IMMORTAL_FAIL, 1); diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp index 23cd3f12a..e6e92f8ce 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp @@ -51,8 +51,7 @@ enum Events EVENT_CRYSTAL_CHAINS = 2, EVENT_TAIL_SWEEP = 3, EVENT_HEALTH_CHECK = 4, - EVENT_ACHIEVEMENT_CHECK = 5, - EVENT_KILL_TALK = 6 + EVENT_ACHIEVEMENT_CHECK = 5 }; struct boss_keristrasza : public BossAI @@ -89,11 +88,7 @@ struct boss_keristrasza : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void SetData(uint32 type, uint32) override diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp index cb8705696..a410d7212 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp @@ -76,8 +76,7 @@ enum Events EVENT_MAGUS_HEALTH2 = 5, EVENT_MAGUS_FAIL_ACHIEVEMENT = 6, EVENT_MAGUS_MERGED = 7, - EVENT_MAGUS_RELOCATE = 8, - EVENT_KILL_TALK = 9 + EVENT_MAGUS_RELOCATE = 8 }; struct boss_magus_telestra : public BossAI @@ -131,11 +130,7 @@ struct boss_magus_telestra : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp index 397f68f44..65b0a8440 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp @@ -49,8 +49,7 @@ enum Events EVENT_ORMOROK_SPELL_REFLECTION = 3, EVENT_ORMOROK_SUMMON = 4, EVENT_ORMOROK_HEALTH = 5, - EVENT_ORMOROK_SUMMON_SPIKES = 6, - EVENT_KILL_TALK = 7 + EVENT_ORMOROK_SUMMON_SPIKES = 6 }; enum Misc @@ -93,11 +92,7 @@ struct boss_ormorok : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp b/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp index 6e667c6b9..7fec8c37e 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp @@ -66,12 +66,11 @@ enum Misc struct boss_gurtogg_bloodboil : public BossAI { - boss_gurtogg_bloodboil(Creature* creature) : BossAI(creature, DATA_GURTOGG_BLOODBOIL), _recentlySpoken(false) { } + boss_gurtogg_bloodboil(Creature* creature) : BossAI(creature, DATA_GURTOGG_BLOODBOIL) { } void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } void JustEngagedWith(Unit* who) override @@ -145,13 +144,7 @@ struct boss_gurtogg_bloodboil : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(SAY_SLAY); } void JustSummoned(Creature* summon) override @@ -171,8 +164,6 @@ struct boss_gurtogg_bloodboil : public BossAI return me->GetHomePosition().GetExactDist2d(me) > 105.0f; } -private: - bool _recentlySpoken; }; class spell_gurtogg_bloodboil : public SpellScript diff --git a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp index 2e0cfe9f0..8a79fd4bb 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp @@ -196,7 +196,7 @@ public: struct boss_illidan_stormrage : public BossAI { - boss_illidan_stormrage(Creature* creature) : BossAI(creature, DATA_ILLIDAN_STORMRAGE), _canTalk(true), _dying(false), _inCutscene(false), beamPosId(0) { } + boss_illidan_stormrage(Creature* creature) : BossAI(creature, DATA_ILLIDAN_STORMRAGE), _dying(false), _inCutscene(false), beamPosId(0) { } void Reset() override { @@ -204,7 +204,6 @@ struct boss_illidan_stormrage : public BossAI me->m_Events.CancelEventGroup(GROUP_BERSERK); me->m_Events.CancelEventGroup(GROUP_PHASE_FLYING); me->m_Events.CancelEventGroup(GROUP_DEMON_FORM); - _canTalk = true; _dying = false; _inCutscene = false; beamPosId = urand(0, MAX_EYE_BEAM_POS - 1); @@ -652,15 +651,7 @@ struct boss_illidan_stormrage : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - Talk(SAY_ILLIDAN_KILL); - _canTalk = false; - - me->m_Events.AddEventAtOffset([&] { - _canTalk = true; - }, 6s); // 3590ms - } + Talk(SAY_ILLIDAN_KILL); } void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask) override @@ -689,7 +680,6 @@ struct boss_illidan_stormrage : public BossAI } private: - bool _canTalk; bool _dying; bool _inCutscene; uint8 beamPosId; diff --git a/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp b/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp index 31c16831a..73f6504b6 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp @@ -98,7 +98,6 @@ enum Misc EVENT_SPELL_VANISH_OUT = 31, EVENT_SPELL_ENRAGE = 32, - EVENT_KILL_TALK = 100 }; class VerasEnvenom : public BasicEvent @@ -262,11 +261,7 @@ struct boss_illidari_council_memberAI : public ScriptedAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_COUNCIL_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_COUNCIL_SLAY); } void JustDied(Unit*) override diff --git a/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp b/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp index 5cfb86a64..40024dc6e 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp @@ -64,13 +64,12 @@ enum Misc struct boss_mother_shahraz : public BossAI { - boss_mother_shahraz(Creature* creature) : BossAI(creature, DATA_MOTHER_SHAHRAZ), _canTalk(true) { } + boss_mother_shahraz(Creature* creature) : BossAI(creature, DATA_MOTHER_SHAHRAZ) { } void Reset() override { _Reset(); me->m_Events.CancelEventGroup(GROUP_ENRAGE); - _canTalk = true; ScheduleHealthCheckEvent(10, [&] { Talk(SAY_EMOTE_FRENZY); @@ -113,15 +112,7 @@ struct boss_mother_shahraz : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - Talk(SAY_SLAY); - _canTalk = false; - - ScheduleUniqueTimedEvent(6s, [&] { - _canTalk = true; - }, 1); - } + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -131,8 +122,6 @@ struct boss_mother_shahraz : public BossAI Talk(SAY_DEATH); } - private: - bool _canTalk; }; class spell_mother_shahraz_random_periodic_aura : public AuraScript diff --git a/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp b/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp index 0827a0e63..f4e9d95bd 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp @@ -263,11 +263,10 @@ public: struct boss_essence_of_sufferingAI : public ScriptedAI { - boss_essence_of_sufferingAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_sufferingAI(Creature* creature) : ScriptedAI(creature) { } void Reset() override { - _recentlySpoken = false; scheduler.CancelAll(); } @@ -311,13 +310,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SUFF_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(SUFF_SAY_SLAY); } void JustEngagedWith(Unit* /*who*/) override @@ -354,8 +347,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; @@ -371,11 +362,10 @@ public: struct boss_essence_of_desireAI : public ScriptedAI { - boss_essence_of_desireAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_desireAI(Creature* creature) : ScriptedAI(creature) { } void Reset() override { - _recentlySpoken = false; scheduler.CancelAll(); } @@ -418,13 +408,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(DESI_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(DESI_SAY_SLAY); } void JustEngagedWith(Unit* /*who*/) override @@ -467,8 +451,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; @@ -484,13 +466,12 @@ public: struct boss_essence_of_angerAI : public ScriptedAI { - boss_essence_of_angerAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_angerAI(Creature* creature) : ScriptedAI(creature) { } ObjectGuid targetGUID; void Reset() override { - _recentlySpoken = false; targetGUID.Clear(); scheduler.CancelAll(); } @@ -507,13 +488,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(ANGER_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(ANGER_SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -563,8 +538,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; diff --git a/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp b/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp index 2cd31e17e..b944ca351 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp @@ -77,7 +77,6 @@ struct boss_teron_gorefiend : public BossAI { boss_teron_gorefiend(Creature* creature) : BossAI(creature, DATA_TERON_GOREFIEND) { - _recentlySpoken = false; _intro = false; } @@ -127,18 +126,9 @@ struct boss_teron_gorefiend : public BossAI BossAI::JustEngagedWith(who); } - void KilledUnit(Unit* victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer()) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - - ScheduleUniqueTimedEvent(6s, [&] - { - _recentlySpoken = false; - }, 1); - } + Talk(SAY_SLAY, victim); } void SetData(uint32 type, uint32 id) override @@ -175,7 +165,6 @@ struct boss_teron_gorefiend : public BossAI } private: - bool _recentlySpoken; bool _intro; }; diff --git a/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp b/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp index 2791c7f04..f817fed19 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp @@ -46,7 +46,6 @@ enum Spells enum Events { - EVENT_TALK_CHECK = 1, EVENT_ENRAGE = 2 }; @@ -57,7 +56,7 @@ enum Misc struct boss_najentus : public BossAI { - boss_najentus(Creature* creature) : BossAI(creature, DATA_HIGH_WARLORD_NAJENTUS), _canTalk(true) { } + boss_najentus(Creature* creature) : BossAI(creature, DATA_HIGH_WARLORD_NAJENTUS) { } void Reset() override { @@ -68,8 +67,6 @@ struct boss_najentus : public BossAI void JustEngagedWith(Unit* who) override { - _canTalk = true; - BossAI::JustEngagedWith(who); Talk(SAY_AGGRO); DoCastSelf(SPELL_REMOVE_SPINES); @@ -108,15 +105,7 @@ struct boss_najentus : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && _canTalk) - { - Talk(SAY_SLAY); - _canTalk = false; - ScheduleUniqueTimedEvent(5s, [&] - { - _canTalk = true; - }, EVENT_TALK_CHECK); - } + Talk(SAY_SLAY, victim); } void JustDied(Unit* killer) override @@ -126,8 +115,6 @@ struct boss_najentus : public BossAI Talk(SAY_DEATH); } -private: - bool _canTalk; }; class spell_najentus_needle_spine : public SpellScript diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp index 353e357cf..36f0d3c4b 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp @@ -92,7 +92,6 @@ struct boss_fathomlord_karathress : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; ScheduleHealthCheckEvent(75, [&]{ instance->DoForAllMinions(DATA_FATHOM_LORD_KARATHRESS, [&](Creature* fathomguard) { @@ -117,15 +116,7 @@ struct boss_fathomlord_karathress : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -182,8 +173,6 @@ struct boss_fathomlord_karathress : public BossAI }); } } -private: - bool _recentlySpoken; }; struct LeechingThrowSelector diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp index d24ca089c..d289b8e17 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp @@ -87,12 +87,11 @@ enum WaterElementalPathIds struct boss_hydross_the_unstable : public BossAI { - boss_hydross_the_unstable(Creature* creature) : BossAI(creature, DATA_HYDROSS_THE_UNSTABLE), _recentlySpoken(false) { } + boss_hydross_the_unstable(Creature* creature) : BossAI(creature, DATA_HYDROSS_THE_UNSTABLE) { } void Reset() override { _Reset(); - _recentlySpoken = false; SummonTaintedElementalOOC(); } @@ -249,15 +248,7 @@ struct boss_hydross_the_unstable : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_SLAY : SAY_CLEAN_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_SLAY : SAY_CLEAN_SLAY); } void JustSummoned(Creature* summon) override @@ -291,8 +282,6 @@ struct boss_hydross_the_unstable : public BossAI Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_DEATH : SAY_CLEAN_DEATH); BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_hydross_cleansing_field_aura : public AuraScript diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp index 265572584..8851ee068 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp @@ -78,7 +78,6 @@ struct boss_lady_vashj : public BossAI void Reset() override { _count = 0; - _recentlySpoken = false; _batTimer = 20s; _playerAngle = 0.0f; BossAI::Reset(); @@ -94,15 +93,7 @@ struct boss_lady_vashj : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -258,7 +249,6 @@ struct boss_lady_vashj : public BossAI private: float _playerAngle; - bool _recentlySpoken; bool _intro; int32 _count; std::chrono::seconds _batTimer; diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp index be599ef83..1b94f747b 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp @@ -83,7 +83,6 @@ struct boss_leotheras_the_blind : public BossAI DoCastSelf(SPELL_CLEAR_CONSUMING_MADNESS, true); DoCastSelf(SPELL_DUAL_WIELD, true); me->SetReactState(REACT_PASSIVE); - _recentlySpoken = false; ScheduleHealthCheckEvent(15, [&]{ me->RemoveAurasDueToSpell(SPELL_WHIRLWIND); @@ -242,8 +241,6 @@ struct boss_leotheras_the_blind : public BossAI me->setAttackTimer(BASE_ATTACK, 2000); } } -private: - bool _recentlySpoken; }; struct npc_inner_demon : public ScriptedAI diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp index a923b81a9..240dc4444 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp @@ -60,21 +60,11 @@ struct boss_morogrim_tidewalker : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } - void KilledUnit(Unit*) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustSummoned(Creature* summon) override @@ -131,8 +121,6 @@ struct boss_morogrim_tidewalker : public BossAI context.Repeat(45s, 60s); }); } -private: - bool _recentlySpoken; }; class spell_morogrim_tidewalker_watery_grave : public SpellScript diff --git a/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp b/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp index 9c0dfcd9c..0dd93ac90 100644 --- a/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp +++ b/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp @@ -67,7 +67,6 @@ struct boss_gruul : public BossAI void Reset() override { _Reset(); - _recentlySpoken = false; _caveInTimer = 29s; } @@ -119,18 +118,9 @@ struct boss_gruul : public BossAI }); } - void KilledUnit(Unit* /*who*/) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -154,7 +144,6 @@ struct boss_gruul : public BossAI private: std::chrono::milliseconds _caveInTimer; - bool _recentlySpoken; }; struct npc_invisible_tractor_beam_source : public NullCreatureAI diff --git a/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp b/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp index ac24a919d..57c4895e1 100644 --- a/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp +++ b/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp @@ -69,7 +69,6 @@ struct boss_high_king_maulgar : public BossAI void Reset() override { _Reset(); - _recentlySpoken = false; me->SetLootMode(0); ScheduleHealthCheckEvent(50, [&]{ Talk(SAY_ENRAGE); @@ -87,18 +86,9 @@ struct boss_high_king_maulgar : public BossAI }); } - void KilledUnit(Unit* /*victim*/) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -158,8 +148,6 @@ struct boss_high_king_maulgar : public BossAI DoMeleeAttackIfReady(); } -private: - bool _recentlySpoken; }; struct boss_olm_the_summoner : public ScriptedAI diff --git a/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp b/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp index edb20d541..f97519f04 100644 --- a/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp +++ b/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp @@ -80,7 +80,6 @@ struct boss_magtheridon : public BossAI BossAI::Reset(); _currentPhase = 0; _castingQuake = false; - _recentlySpoken = false; _magReleased = false; _interruptScheduler.CancelAll(); scheduler.Schedule(90s, [this](TaskContext context) @@ -123,16 +122,7 @@ struct boss_magtheridon : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext /*context*/) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -244,7 +234,6 @@ struct boss_magtheridon : public BossAI private: bool _castingQuake; - bool _recentlySpoken; bool _magReleased; uint8 _currentPhase; TaskScheduler _interruptScheduler; diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp index 54235dda0..8e82904e3 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp @@ -104,10 +104,7 @@ struct boss_high_astromancer_solarian : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - { - Talk(SAY_KILL); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp index 3c4582d46..6b47324c8 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp @@ -52,20 +52,11 @@ struct boss_void_reaver : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -102,8 +93,6 @@ struct boss_void_reaver : public BossAI }); } - private: - bool _recentlySpoken; }; void AddSC_boss_void_reaver() From 677213c077a71cae9480304e70cb26eb66a942aa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:54:26 +0000 Subject: [PATCH 27/67] chore(DB): import pending files Referenced commit(s): d0d970b8f63715c330a88e8c38be560b62c8a84b --- .../creature_text_options.sql => db_world/2026_07_14_03.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/creature_text_options.sql => db_world/2026_07_14_03.sql} (98%) diff --git a/data/sql/updates/pending_db_world/creature_text_options.sql b/data/sql/updates/db_world/2026_07_14_03.sql similarity index 98% rename from data/sql/updates/pending_db_world/creature_text_options.sql rename to data/sql/updates/db_world/2026_07_14_03.sql index efe07af1c..c58bbc258 100644 --- a/data/sql/updates/pending_db_world/creature_text_options.sql +++ b/data/sql/updates/db_world/2026_07_14_03.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_14_02 -> 2026_07_14_03 -- Reusable option sets for creature text groups CREATE TABLE IF NOT EXISTS `creature_text_option_sets` ( `SetID` TINYINT UNSIGNED NOT NULL, From 586775375a212b53d91ac19e69e5f83d2c5a7777 Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Wed, 15 Jul 2026 02:20:27 +0200 Subject: [PATCH 28/67] fix(Scripts/Ulduar): restore Freya chest loot eligibility (#26592) --- .../Northrend/Ulduar/Ulduar/instance_ulduar.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 4cdd6bc11..c2d28b23d 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -624,6 +624,17 @@ public: if (GetBossState(BOSS_FREYA) == DONE) gameObject->SetRespawnTime(7 * DAY); break; + // Freya Loot + case 194324: + case 194325: + case 194326: + case 194327: + case 194328: + case 194329: + case 194330: + case 194331: + gameObject->SetLootRecipient(instance); + break; } } From 142b2ba67cc475de65b42cf21e673473cad9a204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Wed, 15 Jul 2026 02:20:43 +0200 Subject: [PATCH 29/67] fix(Core/Battlegrounds): fill existing BGs instead of starting duplicates (#26585) --- src/server/game/Battlegrounds/BattlegroundMgr.cpp | 2 +- src/server/game/Battlegrounds/BattlegroundQueue.cpp | 3 +-- src/server/game/Handlers/BattleGroundHandler.cpp | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp index c03323e4b..5c2328e43 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp +++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp @@ -948,7 +948,7 @@ BGFreeSlotQueueContainer& BattlegroundMgr::GetBGFreeSlotQueueStore(BattlegroundT void BattlegroundMgr::AddToBGFreeSlotQueue(BattlegroundTypeId bgTypeId, Battleground* bg) { - bgDataStore[bgTypeId].BGFreeSlotQueue.push_front(bg); + bgDataStore[bgTypeId].BGFreeSlotQueue.push_back(bg); } void BattlegroundMgr::RemoveFromBGFreeSlotQueue(BattlegroundTypeId bgTypeId, uint32 instanceId) diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.cpp b/src/server/game/Battlegrounds/BattlegroundQueue.cpp index 1fac5ad54..ad2c7c95e 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.cpp +++ b/src/server/game/Battlegrounds/BattlegroundQueue.cpp @@ -805,8 +805,7 @@ void BattlegroundQueue::BattlegroundQueueUpdate(uint32 diff, BattlegroundTypeId } }; - // battleground with free slot for player should be always in the beggining of the queue - // maybe it would be better to create bgfreeslotqueue for each bracket_id + // fill existing joinable battlegrounds, oldest first, before a new instance may form below BGFreeSlotQueueContainer& bgQueues = sBattlegroundMgr->GetBGFreeSlotQueueStore(bgTypeId); for (BGFreeSlotQueueContainer::iterator itr = bgQueues.begin(); itr != bgQueues.end();) { diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index 9051801e5..07e2863ad 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -554,6 +554,8 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) // leaks forever, permanently skewing team selection and blocking the // empty instance's cleanup. bg->DecreaseInvitedCount(teamId); + if (bg->HasFreeSlots()) + bg->AddToBGFreeSlotQueue(); _player->RemoveBattlegroundQueueId(bgQueueTypeId); _player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL); From 159881d24056c02f5b5c39bff81e6a503b75b82d Mon Sep 17 00:00:00 2001 From: Yuof <6691056+Yuof@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:24:10 +0200 Subject: [PATCH 30/67] fix(Core/Player): remove deleted equip-probe items from the item update queue (#26606) --- src/server/game/Entities/Player/PlayerStorage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 62fc2df4a..308f502a7 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -1863,6 +1863,10 @@ InventoryResult Player::CanEquipNewItem(uint8 slot, uint16& dest, uint32 item, b if (pItem) { InventoryResult result = CanEquipItem(slot, dest, pItem, swap); + // Random-property items queue themselves on creation (SetItemRandomProperties + // -> SetState(ITEM_CHANGED, owner)); the probe must leave the update queue + // before deletion or the queue keeps a pointer to freed memory. + pItem->RemoveFromUpdateQueueOf(const_cast<Player*>(this)); delete pItem; return result; } From 81e60ffd413aeec17b7b75645f8465390dd90bc6 Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Wed, 15 Jul 2026 02:24:21 +0200 Subject: [PATCH 31/67] fix(Scripts/Ulduar): Freya Summon Allies use Summon Wave spells (#26603) --- .../Northrend/Ulduar/Ulduar/boss_freya.cpp | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index ff07548e3..d625e36ef 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -37,6 +37,9 @@ enum FreyaSpells SPELL_TOUCH_OF_EONAR = 62528, SPELL_ATTUNED_TO_NATURE = 62519, SPELL_SUMMON_LIFEBINDER = 62870, + SPELL_SUMMON_WAVE_1 = 62685, // Summon Ancient Conservator + SPELL_SUMMON_WAVE_3 = 62686, // Summon Trio (Water Spirit, Storm Lasher, Snaplasher) + SPELL_SUMMON_WAVE_10 = 62687, // Summon Detonating Lashers SPELL_SUNBEAM = 62623, SPELL_NATURE_BOMB_FLIGHT = 64648, SPELL_NATURE_BOMB_DAMAGE = 64587, @@ -351,26 +354,16 @@ struct boss_freya : public BossAI { case GROUP_TRIO: Talk(SAY_SUMMON_TRIO); - me->SummonCreature(NPC_ANCIENT_WATER_SPIRIT, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_STORM_LASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_SNAPLASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); + DoCast(SPELL_SUMMON_WAVE_3); break; - case GROUP_CONSERVATOR: // Ancient Conservator + case GROUP_CONSERVATOR: Talk(SAY_SUMMON_CONSERVATOR); - me->SummonCreature(NPC_ANCIENT_CONSERVATOR, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); + DoCast(SPELL_SUMMON_WAVE_1); break; - case GROUP_LASHERS: // Detonating Lashers + case GROUP_LASHERS: Talk(SAY_SUMMON_LASHERS); - // Spread the lashers evenly in a ring around Freya instead of - // clustering them in one spot, matching retail. for (uint8 i = 0; i < 10; ++i) - { - float angle = i * 2.0f * float(M_PI) / 10.0f + frand(-0.35f, 0.35f); - float dist = frand(18.0f, 28.0f); - float x = me->GetPositionX() + dist * std::cos(angle); - float y = me->GetPositionY() + dist * std::sin(angle); - me->SummonCreature(NPC_DETONATING_LASHER, x, y, me->GetMapHeight(x, y, me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); - } + DoCast(SPELL_SUMMON_WAVE_10); break; } } From 5d098c97817614d98dff4ed3a978fa63281f1b6b Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:24:38 -0300 Subject: [PATCH 32/67] fix(Scripts/Ulduar): keep Thorim loot chest alive through his outro (#26567) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp index 157873af3..bec73c9d7 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp @@ -550,10 +550,12 @@ struct boss_thorim : public BossAI if (_hardMode) chestId += 1; // hard mode offset - if ((go = me->SummonGameObject(chestId, 2134.73f, -286.32f, 419.51f, 4.65f, 0, 0, 0, 0, 0))) + // Summoned by the map, not Thorim, so the chest survives his despawn during the outro. + if ((go = me->GetMap()->SummonGameObject(chestId, 2134.73f, -286.32f, 419.51f, 4.65f, 0, 0, 0, 0, 0))) { go->ReplaceAllGameObjectFlags((GameObjectFlags)0); go->SetLootRecipient(me->GetMap()); + go->SetRespawnTime(7 * DAY); } // Defeat credit From a47b2baa590a316a66563e79412d650a1c6cada3 Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:25:45 -0400 Subject: [PATCH 33/67] fix(Core/Spell): restore Feign Death threat split outside boss encounters (#26327) Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> --- src/server/game/Spells/Auras/SpellAuraEffects.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index 9c4fa8fe8..2ea95ae4b 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -2989,7 +2989,7 @@ void AuraEffect::HandleFeignDeath(AuraApplication const* aurApp, uint8 mode, boo for (auto& pair : target->GetThreatMgr().GetThreatenedByMeList()) pair.second->ScaleThreat(0.0f); - if (target->GetMap()->IsDungeon()) // feign death does not remove combat in dungeons + if (target->GetInstanceScript() && target->GetInstanceScript()->IsEncounterInProgress()) { target->AttackStop(); if (Player* targetPlayer = target->ToPlayer()) From 82d44f885bb254cb6bcaa24a041df5534006b5bc Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:27:40 +0200 Subject: [PATCH 34/67] feat(Core/Account): Implement ACCOUNT_FLAG_DEATH_KNIGHT_OK (#26344) --- src/common/Common.h | 2 +- src/server/game/Handlers/CharacterHandler.cpp | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/common/Common.h b/src/common/Common.h index a14647b1f..b77fe3760 100644 --- a/src/common/Common.h +++ b/src/common/Common.h @@ -93,7 +93,7 @@ enum AccountFlag ACCOUNT_FLAG_EXPANSION2_COLLECTOR = 0x4000000, // NYI WotLK Collector's Edition ACCOUNT_FLAG_OVERMIND_LINKED = 0x8000000, // NYI Linked with Battle.net account ACCOUNT_FLAG_DEMOS = 0x10000000, // NYI UNK - ACCOUNT_FLAG_DEATH_KNIGHT_OK = 0x20000000, // NYI Has level 55 on account? + ACCOUNT_FLAG_DEATH_KNIGHT_OK = 0x20000000, // Allowed to create Death Knight (account met heroic level requirement on WotLK+) // Below might be StarCraft II related ACCOUNT_FLAG_S2_REQUIRE_IGR = 0x40000000, // NYI UNK ACCOUNT_FLAG_S2_TRIAL = 0x80000000, // NYI UNK diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index d00e00aba..48ed07edc 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -378,7 +378,8 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) // speedup check for heroic class disabled case uint32 req_level_for_heroic = sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_MIN_LEVEL_FOR_HEROIC_CHARACTER); - if (!HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT && req_level_for_heroic > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + if (!HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT + && req_level_for_heroic > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) && !HasAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK)) { SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); return; @@ -445,7 +446,8 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) } bool haveSameRace = false; uint32 heroicReqLevel = sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_MIN_LEVEL_FOR_HEROIC_CHARACTER); - bool hasHeroicReqLevel = (heroicReqLevel == 0); + bool const deathKnightFlagAlreadySet = HasAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK); // Account flag is superior to level requirement. + bool hasHeroicReqLevel = (heroicReqLevel == 0) || deathKnightFlagAlreadySet; bool allowTwoSideAccounts = !sWorld->IsPvPRealm() || sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_ACCOUNTS) || HasPermission(rbac::RBAC_PERM_TWO_SIDE_CHARACTER_CREATION); uint32 skipCinematics = sWorld->getIntConfig(CONFIG_SKIP_CINEMATICS); bool checkDeathKnightReqs = !HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT; @@ -534,10 +536,16 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) } } - if (checkDeathKnightReqs && !hasHeroicReqLevel) + if (checkDeathKnightReqs) { - SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); - return; + if (!deathKnightFlagAlreadySet && hasHeroicReqLevel) + UpdateAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK); + + if (!hasHeroicReqLevel) + { + SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); + return; + } } // Check name uniqueness in the same step as saving to database From 2a544a5d6d178630fc607853761bcf9c8d4e2a33 Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:30:33 -0400 Subject: [PATCH 35/67] fix(Core/Spells): Fire CAST proc before HIT/FINISH for instant spells (#25647) Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> Co-authored-by: Shauren <shauren.trinity@gmail.com> Co-authored-by: sudlud <sudlud@users.noreply.github.com> --- .../rev_1777425238415321551.sql | 3 + src/server/game/Spells/Spell.cpp | 78 +++++++++---------- 2 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1777425238415321551.sql diff --git a/data/sql/updates/pending_db_world/rev_1777425238415321551.sql b/data/sql/updates/pending_db_world/rev_1777425238415321551.sql new file mode 100644 index 000000000..2bea3fef6 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1777425238415321551.sql @@ -0,0 +1,3 @@ +-- Missile Barrage: consume on FINISH instead of CAST; its mods (DURATION, +-- ACTIVATION_TIME, COST) are applied inside handle_immediate, after CAST fires. +UPDATE `spell_proc` SET `SpellPhaseMask` = 4 WHERE `SpellId` = 44401; diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index a5f8bcc1d..4609a074d 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3951,6 +3951,44 @@ void Spell::_cast(bool skipCheck) } } + // CAST -> HIT -> FINISH ordering: fire CAST before handle_immediate so an aura + // applied during HIT (e.g. Arcane Potency from Clearcasting) isn't consumed by + // the same cast. Triggered spells skip this so periodic ticks (Blizzard etc.) + // don't burn cast-charge buffs. + if (m_originalCaster && !IsTriggered()) + { + uint32 procAttacker = m_procAttacker; + if (!procAttacker) + { + bool IsPositive = m_spellInfo->IsPositive(); + if (m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MAGIC) + { + procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG; + } + else + { + procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG; + } + } + + uint32 hitMask = PROC_HIT_NORMAL; + + for (std::list<TargetInfo>::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit) + { + if (ihit->missCondition != SPELL_MISS_NONE) + continue; + + if (!ihit->crit) + continue; + + hitMask |= PROC_HIT_CRITICAL; + break; + } + + Unit::ProcSkillsAndAuras(m_originalCaster, nullptr, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo, + m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST); + } + // Okay, everything is prepared. Now we need to distinguish between immediate and evented delayed spells if ((m_spellInfo->Speed > 0.0f && !m_spellInfo->IsChanneled())/* xinef: we dont need this || m_spellInfo->Id == 14157*/) { @@ -4008,46 +4046,6 @@ void Spell::_cast(bool skipCheck) if (modOwner) modOwner->SetSpellModTakingSpell(this, false); - // Handle procs on cast - only for non-triggered spells - // Triggered spells (from auras, items, etc.) should not fire CAST phase procs - // as they are not player-initiated casts. This prevents issues like Arcane Potency - // charges being consumed by periodic damage effects (e.g., Blizzard ticks). - // Must be called AFTER handle_immediate() so spell mods (like Missile Barrage's - // duration reduction) are applied before the aura is consumed by the proc. - if (m_originalCaster && !IsTriggered()) - { - uint32 procAttacker = m_procAttacker; - if (!procAttacker) - { - bool IsPositive = m_spellInfo->IsPositive(); - if (m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MAGIC) - { - procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG; - } - else - { - procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG; - } - } - - uint32 hitMask = PROC_HIT_NORMAL; - - for (std::list<TargetInfo>::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit) - { - if (ihit->missCondition != SPELL_MISS_NONE) - continue; - - if (!ihit->crit) - continue; - - hitMask |= PROC_HIT_CRITICAL; - break; - } - - Unit::ProcSkillsAndAuras(m_originalCaster, nullptr, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo, - m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST); - } - if (std::vector<int32> const* spell_triggered = sSpellMgr->GetSpellLinked(m_spellInfo->Id)) { for (int32 id : *spell_triggered) From 61a25901c1f10528eed82900bb01d0335dc261e1 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:31:41 -0300 Subject: [PATCH 36/67] fix(Scripts/Ulduar): re-arm Kologarn arms so they can aggro after a wipe (#26569) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- .../Northrend/Ulduar/Ulduar/boss_kologarn.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp index 2965bc4d4..3710026d8 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp @@ -161,8 +161,12 @@ struct boss_kologarn : public BossAI void AttachLeftArm() { - if (Unit* arm = ObjectAccessor::GetCreature(*me, _left)) + if (Creature* arm = ObjectAccessor::GetCreature(*me, _left)) + { arm->SetHealth(arm->GetMaxHealth()); + // Clear the arm's combat-start state so it can re-pull the boss on a subsequent attempt. + arm->AI()->Reset(); + } else if (Creature* accessory = me->SummonCreature(NPC_LEFT_ARM, *me, TEMPSUMMON_MANUAL_DESPAWN)) { accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); @@ -179,8 +183,12 @@ struct boss_kologarn : public BossAI void AttachRightArm() { - if (Unit* arm = ObjectAccessor::GetCreature(*me, _right)) + if (Creature* arm = ObjectAccessor::GetCreature(*me, _right)) + { arm->SetHealth(arm->GetMaxHealth()); + // Clear the arm's combat-start state so it can re-pull the boss on a subsequent attempt. + arm->AI()->Reset(); + } else if (Creature* accessory = me->SummonCreature(NPC_RIGHT_ARM, *me, TEMPSUMMON_MANUAL_DESPAWN)) { accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); From 243e4000427d10bdc862c6bb202d50c3ca15e2d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:31:45 +0000 Subject: [PATCH 37/67] chore(DB): import pending files Referenced commit(s): 2a544a5d6d178630fc607853761bcf9c8d4e2a33 --- .../rev_1777425238415321551.sql => db_world/2026_07_15_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1777425238415321551.sql => db_world/2026_07_15_00.sql} (83%) diff --git a/data/sql/updates/pending_db_world/rev_1777425238415321551.sql b/data/sql/updates/db_world/2026_07_15_00.sql similarity index 83% rename from data/sql/updates/pending_db_world/rev_1777425238415321551.sql rename to data/sql/updates/db_world/2026_07_15_00.sql index 2bea3fef6..2b94fe60d 100644 --- a/data/sql/updates/pending_db_world/rev_1777425238415321551.sql +++ b/data/sql/updates/db_world/2026_07_15_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_14_03 -> 2026_07_15_00 -- Missile Barrage: consume on FINISH instead of CAST; its mods (DURATION, -- ACTIVATION_TIME, COST) are applied inside handle_immediate, after CAST fires. UPDATE `spell_proc` SET `SpellPhaseMask` = 4 WHERE `SpellId` = 44401; From 3487db8e52c0bceddff94d09e1e413f29c2a7e6e Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Wed, 15 Jul 2026 02:33:02 +0200 Subject: [PATCH 38/67] fix(Core/Spells): Allow two paladins to activate the same aura (#26548) --- src/server/game/Spells/Auras/SpellAuras.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/game/Spells/Auras/SpellAuras.cpp b/src/server/game/Spells/Auras/SpellAuras.cpp index 5f2b887f0..d539ea79f 100644 --- a/src/server/game/Spells/Auras/SpellAuras.cpp +++ b/src/server/game/Spells/Auras/SpellAuras.cpp @@ -201,6 +201,11 @@ void AuraApplication::BuildUpdatePacket(ByteBuffer& data, bool remove) const uint32 flags = _flags; if (aura->GetMaxDuration() > 0 && !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR5_DO_NOT_DISPLAY_DURATION)) flags |= AFLAG_DURATION; + + // paladin auras are positive for self-cast only + if (!IsSelfcasted() && aura->GetSpellInfo()->GetSpellSpecific() == SPELL_SPECIFIC_AURA) + flags &= ~AFLAG_POSITIVE; + data << uint8(flags); data << uint8(aura->GetCasterLevel()); // send stack amount for aura which could be stacked (never 0 - causes incorrect display) or charges From 81cf7f92de8e93a61b70a78768f8c673ee5aeadc Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:34:21 -0400 Subject: [PATCH 39/67] fix(Core/Events): correct looping holiday event start time (BG Call to Arms) (#26328) Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> --- src/server/game/Events/GameEventMgr.cpp | 11 +++ .../game/Events/HolidayDateCalculator.cpp | 25 ++++++ .../game/Events/HolidayDateCalculator.h | 5 ++ .../game/Events/HolidayDateCalculatorTest.cpp | 83 +++++++++++++++++++ 4 files changed, 124 insertions(+) diff --git a/src/server/game/Events/GameEventMgr.cpp b/src/server/game/Events/GameEventMgr.cpp index ee987592e..fb1137076 100644 --- a/src/server/game/Events/GameEventMgr.cpp +++ b/src/server/game/Events/GameEventMgr.cpp @@ -1960,6 +1960,17 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) time_t curTime = GameTime::GetGameTime().count(); + if (holiday->Looping) + { + // Looping events (Battleground Call to Arms) carry a single past anchor in the DBC and + // recur via event.Occurence. Anchor to that DBC date so the window keeps the correct phase + // instead of inheriting the wrong time of day from a stale game_event.start_time. + if (time_t start = HolidayDateCalculator::FindLoopingStartTime(holiday->Date[0], stageOffset, event.Occurence, curTime)) + event.Start = start; + + return; + } + if (!singleDate) { time_t start = HolidayDateCalculator::FindStartTimeForStage( diff --git a/src/server/game/Events/HolidayDateCalculator.cpp b/src/server/game/Events/HolidayDateCalculator.cpp index c507f5413..367fd7056 100644 --- a/src/server/game/Events/HolidayDateCalculator.cpp +++ b/src/server/game/Events/HolidayDateCalculator.cpp @@ -671,3 +671,28 @@ time_t HolidayDateCalculator::FindStartTimeForStage(const uint32_t* packedDates, return 0; } + +time_t HolidayDateCalculator::FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, time_t curTime) +{ + if (!packedAnchor) + return 0; + + std::tm timeInfo = {}; + timeInfo.tm_year = static_cast<int>((packedAnchor >> 24) & 0x1F) + 100; // years since 1900 (2000-2031) + timeInfo.tm_mon = static_cast<int>((packedAnchor >> 20) & 0xF); + timeInfo.tm_mday = static_cast<int>((packedAnchor >> 14) & 0x3F) + 1; + timeInfo.tm_hour = static_cast<int>((packedAnchor >> 6) & 0x1F); + timeInfo.tm_min = static_cast<int>(packedAnchor & 0x3F); + timeInfo.tm_sec = 0; + timeInfo.tm_isdst = -1; + + time_t anchor = mktime(&timeInfo) + stageOffset; + + // Roll forward by whole periods to the most recent occurrence, preserving phase. + time_t const period = static_cast<time_t>(occurenceMinutes) * 60; + if (period > 0 && anchor < curTime) + anchor += ((curTime - anchor) / period) * period; + + return anchor; +} diff --git a/src/server/game/Events/HolidayDateCalculator.h b/src/server/game/Events/HolidayDateCalculator.h index 6ed4f6c67..50b9eb909 100644 --- a/src/server/game/Events/HolidayDateCalculator.h +++ b/src/server/game/Events/HolidayDateCalculator.h @@ -107,6 +107,11 @@ public: static time_t FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates, time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime); + // Start time for a looping holiday event (Battleground Call to Arms): rolls the packed anchor + // forward by whole occurenceMinutes periods to the most recent occurrence. Returns 0 if anchor is 0. + static time_t FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, time_t curTime); + private: // Julian Date conversions for lunar calculations static double DateToJulianDay(int year, int month, double day); diff --git a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp index 19084eb6f..fb16fce9f 100644 --- a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp +++ b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp @@ -1428,3 +1428,86 @@ TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero) time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); EXPECT_EQ(result, 0); } + +// ============================================================================ +// FindLoopingStartTime tests (Battleground Call to Arms) +// ============================================================================ + +class FindLoopingStartTimeTest : public ::testing::Test +{ +protected: + static time_t MakeTime(int year, int month, int day, int hour = 0) + { + std::tm t = {}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_isdst = -1; + return mktime(&t); + } + + static uint32_t PackAnchor(int year, int month, int day, int hour, int minute) + { + uint32_t packed = 0; + packed |= (static_cast<uint32_t>(year - 2000) & 0x1F) << 24; + packed |= (static_cast<uint32_t>(month - 1) & 0xF) << 20; + packed |= (static_cast<uint32_t>(day - 1) & 0x3F) << 14; + packed |= (static_cast<uint32_t>(hour) & 0x1F) << 6; + packed |= (static_cast<uint32_t>(minute) & 0x3F); + return packed; + } + + static constexpr uint32_t OCCURENCE_MIN = 60480; // 42 day cycle (AV Call to Arms) +}; + +TEST_F(FindLoopingStartTimeTest, ZeroAnchor_ReturnsZero) +{ + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(0, 0, OCCURENCE_MIN, curTime), 0); +} + +TEST_F(FindLoopingStartTimeTest, FutureAnchor_NotRolled) +{ + uint32_t anchor = PackAnchor(2026, 10, 30, 0, 0); + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime), + MakeTime(2026, 10, 30)); +} + +// The bug: an ancient midnight anchor must roll forward preserving phase, not fall back to a stale start_time. +TEST_F(FindLoopingStartTimeTest, AncientAnchor_RollsToMostRecentOccurrence) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime); + + time_t const period = static_cast<time_t>(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26); + + EXPECT_LE(result, curTime); + EXPECT_GT(result + period, curTime); + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, StageOffsetApplied) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t stageOffset = 96 * 3600; + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, stageOffset, OCCURENCE_MIN, curTime); + + time_t const period = static_cast<time_t>(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26) + stageOffset; + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, ZeroOccurence_ReturnsAnchor) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, 0, curTime), + MakeTime(2007, 10, 26)); +} From 9ac4d5b8bc801eedf9c3e8b221b9e0eeece5e72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Wed, 15 Jul 2026 02:36:13 +0200 Subject: [PATCH 40/67] docs(pr-template): update AI-assisted rules (#26617) Co-authored-by: Kitzunu <24550914+Kitzunu@users.noreply.github.com> --- pull_request_template.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pull_request_template.md b/pull_request_template.md index ca5745a3e..5a573003e 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -13,11 +13,13 @@ This PR proposes changes to: ### AI-assisted Pull Requests > [!IMPORTANT] -> While the use of AI tools when preparing pull requests is not prohibited, contributors must clearly disclose when such tools have been used and specify the model involved. -> -> Contributors are also expected to fully understand the changes they are submitting and must be able to explain and justify those changes when requested by maintainers. +> Using AI tools to prepare pull requests is allowed, but it must be disclosed and it must follow our AC guidelines for AI Agentic Engineering (link below). +> +> You are expected to fully understand the changes you submit and to be able to explain and justify them when maintainers ask. -- [ ] AI tools (e.g. ChatGPT, Claude, or similar) were used entirely or partially in preparing this pull request. Please specify which tools were used, if any. +- [ ] AI tools (e.g. Claude, ChatGPT, or similar) were used entirely or partially to prepare this pull request. If checked, specify which tools and models below. + - Tools/models used: +- [ ] I have read and understood the [AC guidelines for AI Agentic Engineering](https://www.azerothcore.org/wiki/agentic-engineering) ## Issues Addressed: <!-- If your fix has a relating issue, link it below --> From f8b4e180a5d9f9fe4e50d7d6af0bcdaeeec1bd98 Mon Sep 17 00:00:00 2001 From: Allariah <Allariah@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:53:34 +0100 Subject: [PATCH 41/67] fix(Scripts/Ulduar): Iron Roots now hits 3 random players (#26400) Co-authored-by: Skjalf <47818697+Nyeriah@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index d625e36ef..fec84d23f 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -591,7 +591,7 @@ struct boss_freya : public BossAI break; case EVENT_FREYA_IRON_ROOT: Talk(EMOTE_IRON_ROOTS); - me->CastCustomSpell(SPELL_IRON_ROOTS_FREYA, SPELLVALUE_MAX_TARGETS, 1, me, false); + me->CastCustomSpell(SPELL_IRON_ROOTS_FREYA, SPELLVALUE_MAX_TARGETS, me->GetMap()->Is25ManRaid() ? 3 : 1, me, false); events.Repeat(45s, 55s); break; case EVENT_FREYA_UNSTABLE_SUN_BEAM: From 34a8bd6655c02448d3da6195dcd00647f634dde3 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:13:13 +0200 Subject: [PATCH 42/67] =?UTF-8?q?fix(Core/Calendar):=20correct=20malformed?= =?UTF-8?q?=20mail=20subject=20when=20deleting=20a=20ca=E2=80=A6=20(#26615?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/game/Calendar/CalendarMgr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/Calendar/CalendarMgr.cpp b/src/server/game/Calendar/CalendarMgr.cpp index e258e2420..ecf98ef6d 100644 --- a/src/server/game/Calendar/CalendarMgr.cpp +++ b/src/server/game/Calendar/CalendarMgr.cpp @@ -483,7 +483,7 @@ uint32 CalendarMgr::GetPlayerNumPending(ObjectGuid guid) std::string CalendarEvent::BuildCalendarMailSubject(ObjectGuid remover) const { std::ostringstream strm; - strm << remover.ToString() << ':' << _title; + strm << remover.GetRawValue() << ':' << _title; return strm.str(); } From 783df061201f115eab37bae461d5a96e6dd5191a Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:43:49 +0200 Subject: [PATCH 43/67] chore(Script/Commands): Update last commands to RBAC (#26607) --- .../rev_1784028632835376200.sql | 33 +++++++++++++++++++ src/server/game/Accounts/RBAC.h | 13 ++++++++ .../scripts/Commands/cs_autobroadcast.cpp | 9 ++--- src/server/scripts/Commands/cs_mail.cpp | 5 +-- src/server/scripts/Commands/cs_npc.cpp | 8 ++--- src/server/scripts/Commands/cs_pool.cpp | 5 +-- src/server/scripts/Commands/cs_spellinfo.cpp | 9 ++--- 7 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 data/sql/updates/pending_db_auth/rev_1784028632835376200.sql diff --git a/data/sql/updates/pending_db_auth/rev_1784028632835376200.sql b/data/sql/updates/pending_db_auth/rev_1784028632835376200.sql new file mode 100644 index 000000000..8ef345660 --- /dev/null +++ b/data/sql/updates/pending_db_auth/rev_1784028632835376200.sql @@ -0,0 +1,33 @@ +-- RBAC permissions for core commands migrated off deprecated GM security levels. +DELETE FROM `rbac_permissions` WHERE `id` BETWEEN 926 AND 938; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(926, 'Command: autobroadcast list'), +(927, 'Command: autobroadcast add'), +(928, 'Command: autobroadcast locale'), +(929, 'Command: autobroadcast remove'), +(930, 'Command: mail list'), +(931, 'Command: mail return'), +(932, 'Command: npc load'), +(933, 'Command: pool info'), +(934, 'Command: pool lookup'), +(935, 'Command: spellinfo attributes'), +(936, 'Command: spellinfo effects'), +(937, 'Command: spellinfo targets'), +(938, 'Command: spellinfo all'); + +-- Link each command permission to its role: 196 = Administrator role, 197 = Gamemaster role. +DELETE FROM `rbac_linked_permissions` WHERE `linkedId` BETWEEN 926 AND 938; +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(197, 926), +(196, 927), +(196, 928), +(196, 929), +(197, 930), +(197, 931), +(196, 932), +(197, 933), +(197, 934), +(197, 935), +(197, 936), +(197, 937), +(197, 938); diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index 2989bbf5e..8a2a77585 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -687,6 +687,19 @@ enum RBACPermissions RBAC_PERM_COMMAND_CHATFILTER_LIST = 923, RBAC_PERM_COMMAND_CHATFILTER_ADD = 924, RBAC_PERM_COMMAND_CHATFILTER_REMOVE = 925, + RBAC_PERM_COMMAND_AUTOBROADCAST_LIST = 926, + RBAC_PERM_COMMAND_AUTOBROADCAST_ADD = 927, + RBAC_PERM_COMMAND_AUTOBROADCAST_LOCALE = 928, + RBAC_PERM_COMMAND_AUTOBROADCAST_REMOVE = 929, + RBAC_PERM_COMMAND_MAIL_LIST = 930, + RBAC_PERM_COMMAND_MAIL_RETURN = 931, + RBAC_PERM_COMMAND_NPC_LOAD = 932, + RBAC_PERM_COMMAND_POOL_INFO = 933, + RBAC_PERM_COMMAND_POOL_LOOKUP = 934, + RBAC_PERM_COMMAND_SPELLINFO_ATTRIBUTES = 935, + RBAC_PERM_COMMAND_SPELLINFO_EFFECTS = 936, + RBAC_PERM_COMMAND_SPELLINFO_TARGETS = 937, + RBAC_PERM_COMMAND_SPELLINFO_ALL = 938, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/scripts/Commands/cs_autobroadcast.cpp b/src/server/scripts/Commands/cs_autobroadcast.cpp index 27b5a8c20..3173b3b4c 100644 --- a/src/server/scripts/Commands/cs_autobroadcast.cpp +++ b/src/server/scripts/Commands/cs_autobroadcast.cpp @@ -20,6 +20,7 @@ #include "CommandScript.h" #include "Config.h" #include "Language.h" +#include "RBAC.h" using namespace Acore::ChatCommands; @@ -32,10 +33,10 @@ public: { static ChatCommandTable autobroadcastCommandTable = { - { "list", HandleAutobroadcastListCommand, SEC_GAMEMASTER, Console::Yes }, - { "add", HandleAutobroadcastAddCommand, SEC_ADMINISTRATOR, Console::Yes }, - { "locale", HandleAutobroadcastLocaleCommand, SEC_ADMINISTRATOR, Console::Yes }, - { "remove", HandleAutobroadcastRemoveCommand, SEC_ADMINISTRATOR, Console::Yes } + { "list", HandleAutobroadcastListCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_LIST, Console::Yes }, + { "add", HandleAutobroadcastAddCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_ADD, Console::Yes }, + { "locale", HandleAutobroadcastLocaleCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_LOCALE, Console::Yes }, + { "remove", HandleAutobroadcastRemoveCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_REMOVE, Console::Yes } }; static ChatCommandTable commandTable = diff --git a/src/server/scripts/Commands/cs_mail.cpp b/src/server/scripts/Commands/cs_mail.cpp index 3fcf3a2c8..d8f09f85b 100644 --- a/src/server/scripts/Commands/cs_mail.cpp +++ b/src/server/scripts/Commands/cs_mail.cpp @@ -26,6 +26,7 @@ #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" +#include "RBAC.h" #include "ScriptMgr.h" #include "Timer.h" @@ -40,8 +41,8 @@ public: { static ChatCommandTable mailCommandTable = { - { "list", HandleMailListCommand, SEC_GAMEMASTER, Console::Yes }, - { "return", HandleMailReturnCommand, SEC_GAMEMASTER, Console::Yes } + { "list", HandleMailListCommand, rbac::RBAC_PERM_COMMAND_MAIL_LIST, Console::Yes }, + { "return", HandleMailReturnCommand, rbac::RBAC_PERM_COMMAND_MAIL_RETURN, Console::Yes } }; static ChatCommandTable commandTable = diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 28da07703..d44fcfb81 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -200,11 +200,11 @@ public: { "add", npcAddCommandTable }, { "delete", npcDeleteCommandTable }, { "follow", npcFollowCommandTable }, - { "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, Console::Yes }, + { "load", HandleNpcLoadCommand, rbac::RBAC_PERM_COMMAND_NPC_LOAD, Console::Yes }, { "set", npcSetCommandTable }, - { "showloot", HandleNpcShowLootCommand, rbac::RBAC_PERM_COMMAND_NPC_SHOWLOOT, Console::No }, - { "spawngroup", HandleNpcSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No }, - { "despawngroup", HandleNpcDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No } + { "showloot", HandleNpcShowLootCommand, rbac::RBAC_PERM_COMMAND_NPC_SHOWLOOT, Console::No }, + { "spawngroup", HandleNpcSpawnGroupCommand, rbac::RBAC_PERM_COMMAND_NPC_SPAWNGROUP, Console::No }, + { "despawngroup", HandleNpcDespawnGroupCommand, rbac::RBAC_PERM_COMMAND_NPC_DESPAWNGROUP, Console::No } }; static ChatCommandTable commandTable = { diff --git a/src/server/scripts/Commands/cs_pool.cpp b/src/server/scripts/Commands/cs_pool.cpp index 8f56e7063..44661a23a 100644 --- a/src/server/scripts/Commands/cs_pool.cpp +++ b/src/server/scripts/Commands/cs_pool.cpp @@ -25,6 +25,7 @@ #include "ObjectMgr.h" #include "Player.h" #include "PoolMgr.h" +#include "RBAC.h" using namespace Acore::ChatCommands; @@ -37,8 +38,8 @@ public: { static ChatCommandTable poolCommandTable = { - { "info", HandlePoolInfoCommand, SEC_GAMEMASTER, Console::Yes }, - { "lookup", HandlePoolLookupCommand, SEC_GAMEMASTER, Console::No }, + { "info", HandlePoolInfoCommand, rbac::RBAC_PERM_COMMAND_POOL_INFO, Console::Yes }, + { "lookup", HandlePoolLookupCommand, rbac::RBAC_PERM_COMMAND_POOL_LOOKUP, Console::No }, }; static ChatCommandTable commandTable = { diff --git a/src/server/scripts/Commands/cs_spellinfo.cpp b/src/server/scripts/Commands/cs_spellinfo.cpp index f0a1014d9..5b2523ce3 100644 --- a/src/server/scripts/Commands/cs_spellinfo.cpp +++ b/src/server/scripts/Commands/cs_spellinfo.cpp @@ -19,6 +19,7 @@ #include "CommandScript.h" #include "DBCStructure.h" #include "Language.h" +#include "RBAC.h" #include "SmartEnum.h" #include "SpellAuraDefines.h" #include "SpellInfo.h" @@ -35,10 +36,10 @@ public: { static ChatCommandTable spellinfoCommandTable = { - { "attributes", HandleSpellInfoAttributesCommand, SEC_GAMEMASTER, Console::Yes }, - { "effects", HandleSpellInfoEffectsCommand, SEC_GAMEMASTER, Console::Yes }, - { "targets", HandleSpellInfoTargetsCommand, SEC_GAMEMASTER, Console::Yes }, - { "all", HandleSpellInfoAllCommand, SEC_GAMEMASTER, Console::Yes } + { "attributes", HandleSpellInfoAttributesCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_ATTRIBUTES, Console::Yes }, + { "effects", HandleSpellInfoEffectsCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_EFFECTS, Console::Yes }, + { "targets", HandleSpellInfoTargetsCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_TARGETS, Console::Yes }, + { "all", HandleSpellInfoAllCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_ALL, Console::Yes } }; static ChatCommandTable commandTable = From bcf2523a45ed37ff0e95df21837e60b9bf352865 Mon Sep 17 00:00:00 2001 From: Eddy Vega <61223313+Si1ker@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:44:39 -0600 Subject: [PATCH 44/67] fix(Scripts/Ulduar): Algalon stars count and wall-reset (#26612) --- .../Ulduar/Ulduar/boss_algalon_the_observer.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp index c57e0d6cd..5e4f66eac 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp @@ -622,7 +622,7 @@ struct boss_algalon_the_observer : public ScriptedAI bool IsInRoom() { - if (me->GetExactDist2d(&me->GetHomePosition()) > 45.f || me->GetPositionZ() < 410.f) + if (me->GetExactDist2d(&me->GetHomePosition()) > 47.f || me->GetPositionZ() < 410.f) { DoAction(ACTION_ASCEND); return false; @@ -690,12 +690,17 @@ struct boss_algalon_the_observer : public ScriptedAI events.Repeat(15s + 500ms); break; case EVENT_SUMMON_COLLAPSING_STAR: + { Talk(SAY_ALGALON_COLLAPSING_STAR); Talk(EMOTE_ALGALON_COLLAPSING_STAR); - for (uint8 i = 0; i < COLLAPSING_STAR_COUNT; ++i) - me->SummonCreature(NPC_COLLAPSING_STAR, CollapsingStarPos[i], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2000); + uint8 activeStars = summons.GetEntryCount(NPC_COLLAPSING_STAR); + if (activeStars < COLLAPSING_STAR_COUNT) + for (uint8 i = activeStars; i < COLLAPSING_STAR_COUNT; ++i) + me->SummonCreature(NPC_COLLAPSING_STAR, CollapsingStarPos[i], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2000); + events.Repeat(1min); break; + } case EVENT_COSMIC_SMASH: Talk(EMOTE_ALGALON_COSMIC_SMASH); me->CastCustomSpell(SPELL_COSMIC_SMASH, SPELLVALUE_MAX_TARGETS, RAID_MODE(1, 3), (Unit*)nullptr); From ecd1366f09374cb98628b892eedc758158200e6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:44:47 +0000 Subject: [PATCH 45/67] chore(DB): import pending files Referenced commit(s): 783df061201f115eab37bae461d5a96e6dd5191a --- .../rev_1784028632835376200.sql => db_auth/2026_07_15_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_auth/rev_1784028632835376200.sql => db_auth/2026_07_15_00.sql} (95%) diff --git a/data/sql/updates/pending_db_auth/rev_1784028632835376200.sql b/data/sql/updates/db_auth/2026_07_15_00.sql similarity index 95% rename from data/sql/updates/pending_db_auth/rev_1784028632835376200.sql rename to data/sql/updates/db_auth/2026_07_15_00.sql index 8ef345660..1a73100b2 100644 --- a/data/sql/updates/pending_db_auth/rev_1784028632835376200.sql +++ b/data/sql/updates/db_auth/2026_07_15_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_06_01_00 -> 2026_07_15_00 -- RBAC permissions for core commands migrated off deprecated GM security levels. DELETE FROM `rbac_permissions` WHERE `id` BETWEEN 926 AND 938; INSERT INTO `rbac_permissions` (`id`, `name`) VALUES From 53c0500b09f437b67deaa446ff6199c852b29c57 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:02:41 -0300 Subject: [PATCH 46/67] fix(DB/Spell): Watcher Gashra's Enrage should be a buff (#26624) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- .../updates/pending_db_world/rev_1784087165100029500.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784087165100029500.sql diff --git a/data/sql/updates/pending_db_world/rev_1784087165100029500.sql b/data/sql/updates/pending_db_world/rev_1784087165100029500.sql new file mode 100644 index 000000000..b92f52eb2 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784087165100029500.sql @@ -0,0 +1,6 @@ +-- Watcher Gashra's Enrage (52470) is a self-buff, but its -50% physical damage +-- done effect (EFFECT_2) makes the core classify the whole aura as negative, +-- so it shows as a debuff on the target frame. +-- Mark all three effects positive (SPELL_ATTR0_CU_POSITIVE_EFF0 | EFF1 | EFF2). +DELETE FROM `spell_custom_attr` WHERE `spell_id` = 52470; +INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES (52470, 0x0E000000); From 8595299b0f72f98d9d915218d49f737880179fec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:25:49 +0000 Subject: [PATCH 47/67] chore(DB): import pending files Referenced commit(s): 53c0500b09f437b67deaa446ff6199c852b29c57 --- .../rev_1784087165100029500.sql => db_world/2026_07_15_01.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784087165100029500.sql => db_world/2026_07_15_01.sql} (90%) diff --git a/data/sql/updates/pending_db_world/rev_1784087165100029500.sql b/data/sql/updates/db_world/2026_07_15_01.sql similarity index 90% rename from data/sql/updates/pending_db_world/rev_1784087165100029500.sql rename to data/sql/updates/db_world/2026_07_15_01.sql index b92f52eb2..ed08f863c 100644 --- a/data/sql/updates/pending_db_world/rev_1784087165100029500.sql +++ b/data/sql/updates/db_world/2026_07_15_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_15_00 -> 2026_07_15_01 -- Watcher Gashra's Enrage (52470) is a self-buff, but its -50% physical damage -- done effect (EFFECT_2) makes the core classify the whole aura as negative, -- so it shows as a debuff on the target frame. From 0e2b59ba1312f727300658b565118932c246cecb Mon Sep 17 00:00:00 2001 From: OneZero <70149485+OneZero1010@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:01:10 +0200 Subject: [PATCH 48/67] fix(Scripts/Kalimdor): move hardcoded npc_plucky gossip to DB (#26629) Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> --- .../updates/pending_db_world/rev_1784097224106013850.sql | 4 ++++ src/server/scripts/Kalimdor/zone_thousand_needles.cpp | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1784097224106013850.sql diff --git a/data/sql/updates/pending_db_world/rev_1784097224106013850.sql b/data/sql/updates/pending_db_world/rev_1784097224106013850.sql new file mode 100644 index 000000000..acc4eb4f2 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784097224106013850.sql @@ -0,0 +1,4 @@ +-- +DELETE FROM `gossip_menu_option` WHERE `MenuID` = 6626 AND `OptionID` = 0; +INSERT INTO `gossip_menu_option` (`MenuID`, `OptionID`, `OptionIcon`, `OptionText`, `OptionBroadcastTextID`, `OptionType`, `OptionNpcFlag`, `ActionMenuID`, `ActionPoiID`, `BoxCoded`, `BoxMoney`, `BoxText`, `BoxBroadcastTextID`) VALUES +(6626, 0, 0, 'Please tell me the Phrase..', 0, 0, 0, 0, 0, 0, 0, '', 0); diff --git a/src/server/scripts/Kalimdor/zone_thousand_needles.cpp b/src/server/scripts/Kalimdor/zone_thousand_needles.cpp index 9c60689c5..c76108dac 100644 --- a/src/server/scripts/Kalimdor/zone_thousand_needles.cpp +++ b/src/server/scripts/Kalimdor/zone_thousand_needles.cpp @@ -202,13 +202,14 @@ public: # npc_plucky ######*/ -#define GOSSIP_P "Please tell me the Phrase.." - enum Plucky { QUEST_SCOOP = 1950, SPELL_PLUCKY_HUMAN = 9192, - SPELL_PLUCKY_CHICKEN = 9220 + SPELL_PLUCKY_CHICKEN = 9220, + + GOSSIP_MENU_PLUCKY = 6626, + GOSSIP_OPTION_PHRASE = 0 }; class npc_plucky : public CreatureScript @@ -232,7 +233,7 @@ public: bool OnGossipHello(Player* player, Creature* creature) override { if (player->GetQuestStatus(QUEST_SCOOP) == QUEST_STATUS_INCOMPLETE) - AddGossipItemFor(player, GOSSIP_ICON_CHAT, GOSSIP_P, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); + AddGossipItemFor(player, GOSSIP_MENU_PLUCKY, GOSSIP_OPTION_PHRASE, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); SendGossipMenuFor(player, 738, creature->GetGUID()); From 8058c98e185bda972dc38f7dda5be9bbc246015c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:02:22 +0000 Subject: [PATCH 49/67] chore(DB): import pending files Referenced commit(s): 0e2b59ba1312f727300658b565118932c246cecb --- .../rev_1784097224106013850.sql => db_world/2026_07_15_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784097224106013850.sql => db_world/2026_07_15_02.sql} (89%) diff --git a/data/sql/updates/pending_db_world/rev_1784097224106013850.sql b/data/sql/updates/db_world/2026_07_15_02.sql similarity index 89% rename from data/sql/updates/pending_db_world/rev_1784097224106013850.sql rename to data/sql/updates/db_world/2026_07_15_02.sql index acc4eb4f2..0dcbfa2b3 100644 --- a/data/sql/updates/pending_db_world/rev_1784097224106013850.sql +++ b/data/sql/updates/db_world/2026_07_15_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_15_01 -> 2026_07_15_02 -- DELETE FROM `gossip_menu_option` WHERE `MenuID` = 6626 AND `OptionID` = 0; INSERT INTO `gossip_menu_option` (`MenuID`, `OptionID`, `OptionIcon`, `OptionText`, `OptionBroadcastTextID`, `OptionType`, `OptionNpcFlag`, `ActionMenuID`, `ActionPoiID`, `BoxCoded`, `BoxMoney`, `BoxText`, `BoxBroadcastTextID`) VALUES From 4932429b1fd43d469cfe8eaad4d49cd3663cee40 Mon Sep 17 00:00:00 2001 From: Tom Trumper <tom-trumper@hotmail.co.uk> Date: Wed, 15 Jul 2026 11:23:11 +0100 Subject: [PATCH 50/67] fix(apps/docker): Include build ARGs in client-data (#25737) --- apps/docker/Dockerfile | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/docker/Dockerfile b/apps/docker/Dockerfile index 0ad7080bf..8b4ff60b2 100644 --- a/apps/docker/Dockerfile +++ b/apps/docker/Dockerfile @@ -225,9 +225,21 @@ LABEL description="AzerothCore client-data" ENV DATAPATH=/azerothcore/env/dist/data RUN apt-get update && \ - apt-get install -y curl unzip && \ + apt-get install -y curl unzip adduser && \ rm -rf /var/lib/apt/lists/* +ARG USER_ID=1000 +ARG GROUP_ID=1000 +ARG DOCKER_USER=acore + +# To use GID/UID 1000 in ubuntu > 23.04 the existing user must be deleted +# See https://bugs.launchpad.net/cloud-images/+bug/2005129 +RUN userdel --remove ubuntu \ + && addgroup --gid "$GROUP_ID" "$DOCKER_USER" \ + && adduser --disabled-password --gecos '' --uid "$USER_ID" --gid "$GROUP_ID" "$DOCKER_USER" \ + && passwd -d "$DOCKER_USER" \ + && chown -R "$DOCKER_USER:$DOCKER_USER" /azerothcore + COPY --chown=$DOCKER_USER:$DOCKER_USER apps apps VOLUME /azerothcore/env/dist/data From 7ec14c067d0d86983f6078f91a8ba512bda7dd7a Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:41:04 -0300 Subject: [PATCH 51/67] fix(Core/Vehicles): protect Salvaged Siege Engine extra passengers from Flame Leviathan AoE (#26622) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- src/server/game/Spells/SpellInfoCorrections.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index f5e22058c..7b91f5115 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -5345,6 +5345,13 @@ void SpellMgr::LoadSpellInfoCorrections() vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(1520)); // Wyrmrest Vanquisher vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + // Salvaged Siege Engine (Ulduar) extra passenger seats: the only salvaged vehicle seats + // without this flag, leaving those passengers targetable by Flame Leviathan's AoE + vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(4026)); + vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(4027)); + vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + // pussywizard: fix z offset for some vehicles: vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(6206)); // Marrowgar - Bone Spike vse->m_attachmentOffsetZ = 4.0f; From b330948f351cc73b08c9fc80a8793a5283ad071f Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Wed, 15 Jul 2026 13:12:17 +0200 Subject: [PATCH 52/67] fix(Scripts/Ulduar): limit Vezax druid clearcasting immune to resto spec (#26630) --- .../scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp index da84260a2..c41b28006 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp @@ -418,8 +418,9 @@ class spell_aura_of_despair_aura : public AuraScript target->CastSpell(target, SPELL_AURA_OF_DESPAIR_2, true); - // Patch 3.2.0: Omen of Clarity can no longer trigger Clearcasting during the encounter - target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, true); + // Resto druids: Omen of Clarity can no longer trigger Clearcasting (Patch 3.2.0) + if (target->ToPlayer()->GetSpec() == TALENT_TREE_DRUID_RESTORATION) + target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, true); if (target->HasSpell(SPELL_SHAMANISTIC_RAGE)) caster->CastSpell(target, SPELL_CORRUPTED_RAGE, true); From a23dd1d385539f47dc5db060438bd5c0c264e3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Wed, 15 Jul 2026 14:21:52 +0200 Subject: [PATCH 53/67] ci(build): support Ubuntu 26.04 (#25836) Co-authored-by: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Co-authored-by: sudlud <sudlud@users.noreply.github.com> --- .github/SECURITY.md | 8 ++--- .github/actions/linux-build/action.yml | 23 +++++--------- .github/workflows/core-build-nopch.yml | 8 ++--- .github/workflows/core-build-pch.yml | 8 ++--- .github/workflows/dashboard-ci.yml | 4 +-- apps/installer/includes/os_configs/ubuntu.sh | 33 +++++++++++++------- deps/g3dlite/include/G3D/platform.h | 2 +- src/test/mocks/SpellInfoTestHelper.h | 5 +-- 8 files changed, 45 insertions(+), 46 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 00d09000e..f2de9c7cc 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -23,10 +23,10 @@ We support the following versions of dependencies. ### Supported Operating Systems | Linux (Ubuntu) | Status | Recommended | -| :------------- | :----------------: | :------------------: | -| 24.04 | :white_check_mark: | :large_blue_diamond: | -| 22.04 | :white_check_mark: | | -| 20.04 ≤ | :red_circle: | | +|:---------------| :----------------: | :------------------: | +| 26.04 | :white_check_mark: | :large_blue_diamond: | +| 24.04 | :white_check_mark: | | +| 22.04 ≤ | :red_circle: | | | macOS | Status | Recommended | | :---- | :----------------: | :------------------: | diff --git a/.github/actions/linux-build/action.yml b/.github/actions/linux-build/action.yml index 43d6fd29f..20bc9d1f5 100644 --- a/.github/actions/linux-build/action.yml +++ b/.github/actions/linux-build/action.yml @@ -64,23 +64,14 @@ runs: run: | sudo apt update sudo apt remove needrestart #refer: https://github.com/actions/runner-images/issues/9937 - sudo apt-get -y install ccache clang cmake curl google-perftools \ - libmysqlclient-dev make unzip build-essential cmake-data \ - libboost-all-dev libbz2-dev libncurses5-dev libmysql++-dev \ + sudo apt-get -y install ccache clang cmake curl \ + make unzip build-essential cmake-data \ + libboost-all-dev libbz2-dev libmysql++-dev \ libreadline6-dev libssl-dev libtool openssl zlib1g-dev - - # Account for https://github.com/actions/runner-images/issues/8659 - # based off of https://github.com/actions/runner-images/issues/8659#issuecomment-1852353116 - UBUNTU_VERSION="$(grep VERSION_ID /etc/os-release | cut -f2 -d\")" - source /etc/os-release - if [[ "$VERSION_CODENAME" == "jammy" ]]; then - if [[ "${{ inputs.CC }}" =~ "clang-" ]]; then - CLANG_VERSION="$(echo '${{ inputs.CC }}' | cut -f2 -d\-)" - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo add-apt-repository "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-$CLANG_VERSION main" - sudo apt-get -qq update - sudo apt-get -qq install '${{ inputs.CC }}' - fi + if [[ "$(lsb_release -sr)" == "26.04" ]]; then + sudo apt-get -y install libgoogle-perftools-dev default-libmysqlclient-dev + else + sudo apt-get -y install google-perftools libmysqlclient-dev libncurses5-dev fi - name: setup ccache diff --git a/.github/workflows/core-build-nopch.yml b/.github/workflows/core-build-nopch.yml index 56f0a6a2d..3cfcc6254 100644 --- a/.github/workflows/core-build-nopch.yml +++ b/.github/workflows/core-build-nopch.yml @@ -30,10 +30,6 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - compiler: - CC: clang-15 - CXX: clang++-15 - os: ubuntu-24.04 compiler: CC: clang-18 @@ -42,6 +38,10 @@ jobs: compiler: CC: gcc-14 CXX: g++-14 + - os: ubuntu-26.04 + compiler: + CC: clang-20 + CXX: clang++-20 runs-on: ${{ matrix.os }} name: ${{ matrix.os }}-${{ matrix.compiler.CC }}-nopch if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft diff --git a/.github/workflows/core-build-pch.yml b/.github/workflows/core-build-pch.yml index 3ac8cc6fd..b50d4f4b8 100644 --- a/.github/workflows/core-build-pch.yml +++ b/.github/workflows/core-build-pch.yml @@ -30,14 +30,14 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - compiler: - CC: clang-15 - CXX: clang++-15 - os: ubuntu-24.04 compiler: CC: clang-18 CXX: clang++-18 + - os: ubuntu-26.04 + compiler: + CC: clang-20 + CXX: clang++-20 runs-on: ${{ matrix.os }} name: ${{ matrix.os }}-${{ matrix.compiler.CC }}-pch if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 9bd89eced..9e5ca3872 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -41,8 +41,8 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - os: ubuntu-24.04 + - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: @@ -73,8 +73,8 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - os: ubuntu-24.04 + - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: diff --git a/apps/installer/includes/os_configs/ubuntu.sh b/apps/installer/includes/os_configs/ubuntu.sh index c2c84fff3..61288d571 100644 --- a/apps/installer/includes/os_configs/ubuntu.sh +++ b/apps/installer/includes/os_configs/ubuntu.sh @@ -12,15 +12,15 @@ fi UBUNTU_VERSION=$(lsb_release -sr); case $UBUNTU_VERSION in - "22.04") - ;; "24.04") ;; + "26.04") + ;; *) echo "########## ########## ##########" echo "" echo " using unsupported Ubuntu version " $UBUNTU_VERSION - echo " please update to Ubuntu 22.04 or later" + echo " please update to Ubuntu 24.04 or later" echo "" echo "########## ########## ##########" ;; @@ -30,21 +30,32 @@ $SUDO apt update # shared deps DEBIAN_FRONTEND="noninteractive" $SUDO \ -apt-get -y install ccache clang cmake curl google-perftools libmysqlclient-dev make unzip jq screen tmux \ - libreadline-dev libncurses5-dev libncursesw5-dev libbz2-dev git gcc g++ libssl-dev \ +apt-get -y install ccache clang cmake curl make unzip jq screen tmux \ + libreadline-dev libbz2-dev git gcc g++ libssl-dev \ libncurses-dev libboost-all-dev gdb gdbserver expect +# version-specific deps +if [[ "$UBUNTU_VERSION" == "26.04" ]]; then + DEBIAN_FRONTEND="noninteractive" $SUDO \ + apt-get -y install libgoogle-perftools-dev default-libmysqlclient-dev +else + DEBIAN_FRONTEND="noninteractive" $SUDO \ + apt-get -y install google-perftools libmysqlclient-dev libncurses5-dev libncursesw5-dev +fi + VAR_PATH="$CURRENT_PATH/../../../../var" # Do not install MySQL if we are in docker (It will be used a docker container instead) or we are explicitly skipping it. if [[ $DOCKER != 1 && $SKIP_MYSQL_INSTALL != 1 ]]; then - # run noninteractive install for MYSQL 8.4 LTS - wget https://dev.mysql.com/get/mysql-apt-config_0.8.35-1_all.deb -P "$VAR_PATH" - # resolve expired key issue - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A8D3785C - DEBIAN_FRONTEND="noninteractive" $SUDO dpkg -i "$VAR_PATH/mysql-apt-config_0.8.35-1_all.deb" - $SUDO apt-get update + if [[ "$UBUNTU_VERSION" == "24.04" ]]; then + # run noninteractive install for MYSQL 8.4 LTS + wget https://dev.mysql.com/get/mysql-apt-config_0.8.35-1_all.deb -P "$VAR_PATH" + # resolve expired key issue + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A8D3785C + DEBIAN_FRONTEND="noninteractive" $SUDO dpkg -i "$VAR_PATH/mysql-apt-config_0.8.35-1_all.deb" + $SUDO apt-get update + fi DEBIAN_FRONTEND="noninteractive" $SUDO apt-get install -y mysql-server fi diff --git a/deps/g3dlite/include/G3D/platform.h b/deps/g3dlite/include/G3D/platform.h index d043f2149..1c660efef 100644 --- a/deps/g3dlite/include/G3D/platform.h +++ b/deps/g3dlite/include/G3D/platform.h @@ -360,7 +360,7 @@ int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw) {\ // Bring in shared_ptr and weak_ptr #if (defined(__GNUC__) && defined(__APPLE__)) || defined(__linux__) -#include <ciso646> // Defines _LIBCC_VERSION if linking against libc++ or does nothing +#include <version> // Defines _LIBCC_VERSION if linking against libc++ or does nothing #endif #if (!defined(_LIBCPP_VERSION) && defined(__APPLE__)) || (!defined(_LIBCPP_VERSION) && defined(__linux__)) # include <tr1/memory> diff --git a/src/test/mocks/SpellInfoTestHelper.h b/src/test/mocks/SpellInfoTestHelper.h index 2b5a14eca..436135be1 100644 --- a/src/test/mocks/SpellInfoTestHelper.h +++ b/src/test/mocks/SpellInfoTestHelper.h @@ -32,9 +32,6 @@ class TestSpellEntryHelper public: TestSpellEntryHelper() { - // Zero initialize all fields - std::memset(&_entry, 0, sizeof(_entry)); - // Set safe defaults _entry.EquippedItemClass = -1; _entry.SchoolMask = SPELL_SCHOOL_MASK_NORMAL; @@ -154,7 +151,7 @@ public: } private: - SpellEntry _entry; + SpellEntry _entry{}; }; /** From fd04d947594a8573a8f480cb001c2eec3e86dac8 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:26:35 -0300 Subject: [PATCH 54/67] fix(Scripts/Spells): defer aura mutations out of proc-check handlers (#26644) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- src/server/game/Spells/SpellScript.h | 2 ++ .../botanica/instance_the_botanica.cpp | 14 +++++++++++-- src/server/scripts/Spells/spell_mage.cpp | 20 +++++++++++++++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/server/game/Spells/SpellScript.h b/src/server/game/Spells/SpellScript.h index 0ca94edb2..e13ce56cb 100644 --- a/src/server/game/Spells/SpellScript.h +++ b/src/server/game/Spells/SpellScript.h @@ -823,12 +823,14 @@ public: #define AuraEffectSplitFn(F, I) EffectSplitFunction(&F, I) // executed when aura checks if it can proc + // do not use this hook for triggering spellcasts/removing auras etc - runs mid proc-iteration and may be unsafe // example: DoCheckProc += AuraCheckProcFn(class::function); // where function is: bool function (ProcEventInfo& eventInfo); HookList<CheckProcHandler> DoCheckProc; #define AuraCheckProcFn(F) CheckProcHandlerFunction(&F) // executed when aura effect checks if it can proc the aura + // do not use this hook for triggering spellcasts/removing auras etc - runs mid proc-iteration and may be unsafe // example: DoCheckEffectProc += AuraCheckEffectProcFn(class::function, EffectIndexSpecifier, EffectAuraNameSpecifier); // where function is: bool function (AuraEffect const* aurEff, ProcEventInfo& eventInfo); HookList<CheckEffectProcHandler> DoCheckEffectProc; diff --git a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp index ad0f03273..12d8c8431 100644 --- a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp +++ b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp @@ -127,9 +127,19 @@ class spell_botanica_shift_form_aura : public AuraScript { _swapTime = GameTime::GetGameTime().count() + 6; _lastSchool = spellInfo->GetSchoolMask(); - GetUnitOwner()->RemoveAurasDueToSpell(_lastForm); + + // Runs from Aura::GetProcEffectMask while the proc engine iterates the owner's + // applied-aura map. Swapping forms mutates that map (RemoveAura + CastSpell), + // which invalidates the live iterator now that aura containers are flat_multimaps. + // Defer the swap to the owner's event queue so it runs outside the proc walk. + Unit* owner = GetUnitOwner(); + uint32 const oldForm = _lastForm; _lastForm = form; - GetUnitOwner()->CastSpell(GetUnitOwner(), _lastForm, true); + owner->m_Events.AddEventAtOffset([owner, oldForm, form]() + { + owner->RemoveAurasDueToSpell(oldForm); + owner->CastSpell(owner, form, true); + }, 1ms); } } diff --git a/src/server/scripts/Spells/spell_mage.cpp b/src/server/scripts/Spells/spell_mage.cpp index b6a5cf812..ef3b49e36 100644 --- a/src/server/scripts/Spells/spell_mage.cpp +++ b/src/server/scripts/Spells/spell_mage.cpp @@ -123,16 +123,21 @@ class spell_mage_burning_determination : public AuraScript if (!(eventInfo.GetSpellInfo()->GetAllEffectsMechanicMask() & ((1ULL << MECHANIC_INTERRUPT) | (1ULL << MECHANIC_SILENCE)))) return false; + Unit* target = eventInfo.GetActionTarget(); + + // This hook runs while the proc engine iterates the target's aura map; removing an + // aura inline would invalidate the live iterator (aura containers are flat_multimaps). + // Defer removals to the target's event queue so they run outside the proc walk. // Xinef: immuned effect should just eat charge if (eventInfo.GetHitMask() & PROC_EX_IMMUNE) { - eventInfo.GetActionTarget()->RemoveAurasDueToSpell(54748); + target->m_Events.AddEventAtOffset([target]() { target->RemoveAurasDueToSpell(54748); }, 1ms); return false; } - if (Aura* aura = eventInfo.GetActionTarget()->GetAura(54748)) + if (Aura* aura = target->GetAura(54748)) { if (aura->GetDuration() < aura->GetMaxDuration()) - eventInfo.GetActionTarget()->RemoveAurasDueToSpell(54748); + target->m_Events.AddEventAtOffset([target]() { target->RemoveAurasDueToSpell(54748); }, 1ms); return false; } @@ -1045,7 +1050,14 @@ class spell_mage_combustion : public AuraScript // Do not take charges, add a stack of crit buff if (!(eventInfo.GetHitMask() & PROC_HIT_CRITICAL)) { - eventInfo.GetActor()->CastSpell(static_cast<Unit*>(nullptr), SPELL_MAGE_COMBUSTION_PROC, true); + // Applying the stack mutates the actor's aura map while the proc engine iterates + // it (this hook runs from Aura::GetProcEffectMask); defer it so the insert can't + // invalidate the live iterator (aura containers are flat_multimaps). + Unit* actor = eventInfo.GetActor(); + actor->m_Events.AddEventAtOffset([actor]() + { + actor->CastSpell(static_cast<Unit*>(nullptr), SPELL_MAGE_COMBUSTION_PROC, true); + }, 1ms); return false; } From 65443de1cd2f715ce1970a13244cdb44e924eef7 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:30:23 -0300 Subject: [PATCH 55/67] fix(Scripts/Ulduar): keep VX-001 arms deployed during Mimiron phase 4 (#26623) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- .../Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index c0f979d77..95644214b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -952,6 +952,14 @@ struct npc_ulduar_leviathan_mkii : public ScriptedAI _events.Reset(); } + void AttackStart(Unit* who) override + { + ScriptedAI::AttackStart(who); + // Unit::Attack clears the emote state on target switch, which would retract VX-001's arms + if (_phase == 4) + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); + } + void SetData(uint32 id, uint32 value) override { if (id == 1) // setting phase to start fighting @@ -985,6 +993,8 @@ struct npc_ulduar_leviathan_mkii : public ScriptedAI if (Unit* target = SelectTargetFromPlayerList(75.0f)) AttackStart(target); DoZoneInCombat(); + // The assembled V0-L7R-ON animates through its vehicle base; this state keeps VX-001's arms deployed + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); _events.Reset(); _events.ScheduleEvent(EVENT_SPELL_SHOCK_BLAST, 20s); _events.ScheduleEvent(EVENT_PROXIMITY_MINES_1, 6s); @@ -1380,7 +1390,8 @@ struct npc_ulduar_vx001 : public ScriptedAI if (Unit* vb = me->GetVehicleBase()) { vb->SendMeleeAttackStop(); - vb->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + // Keep the arms-deployed state so the model returns to it after the one-shot pulse animation + vb->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); if (!_leftArm) { From c60a4bde661edc3ecc5e39616c970e78544eb531 Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Thu, 16 Jul 2026 00:31:05 +0200 Subject: [PATCH 56/67] fix(Scripts/Ulduar): Update Yogg portal spawning, p1 Sara Shadow Nova (#26632) --- .../rev_1784123420947336369.sql | 5 ++ .../rev_1784123661594956864.sql | 7 ++ .../game/Spells/SpellInfoCorrections.cpp | 6 -- .../Ulduar/Ulduar/boss_yoggsaron.cpp | 75 +++++++++---------- 4 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1784123420947336369.sql create mode 100644 data/sql/updates/pending_db_world/rev_1784123661594956864.sql diff --git a/data/sql/updates/pending_db_world/rev_1784123420947336369.sql b/data/sql/updates/pending_db_world/rev_1784123420947336369.sql new file mode 100644 index 000000000..cf0e67ff7 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784123420947336369.sql @@ -0,0 +1,5 @@ +-- Yogg Portal 'Descend Into Madness' +-- Movement flags: DisableGravity + Root +DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 34072); +INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES +(34072, 0, 0, 1, 1, 0, 0, 0); diff --git a/data/sql/updates/pending_db_world/rev_1784123661594956864.sql b/data/sql/updates/pending_db_world/rev_1784123661594956864.sql new file mode 100644 index 000000000..29a0a0f09 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784123661594956864.sql @@ -0,0 +1,7 @@ +-- Sara should start friendly, becoming hostile when Yogg-Saron reveals himself (phase 2) +UPDATE `creature_template` SET `faction` = 35 WHERE `entry` IN (33134, 34332); + +-- Sara (1) +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 65719) AND (`SourceId` = 0) AND (`ElseGroup` = 1) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 34332) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 65719, 0, 1, 31, 0, 3, 34332, 0, 0, 0, 0, '', 'Shadow Nova'); diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index 7b91f5115..e155138f6 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -2057,12 +2057,6 @@ void SpellMgr::LoadSpellInfoCorrections() spellInfo->AttributesEx6 |= SPELL_ATTR6_IGNORE_PHASE_SHIFT; }); - // Shadow Nova - ApplySpellFix({ 62714, 65209 }, [](SpellInfo* spellInfo) - { - spellInfo->AttributesEx3 |= SPELL_ATTR3_ALWAYS_HIT; - }); - // Cosmic Smash (Algalon the Observer) ApplySpellFix({ 62293 }, [](SpellInfo* spellInfo) { diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp index 08835f46c..812c72143 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp @@ -49,6 +49,8 @@ enum YoggSpells SPELL_TITANIC_STORM_PASSIVE = 64171, SPELL_WEAKENED = 64162, + SPELL_TELEPORT_PORTAL_VISUAL = 64416, + // GLOBAL SPELL_SANITY_BASE = 63786, SPELL_SANITY = 63050, @@ -70,6 +72,7 @@ enum YoggSpells // GUARDIANS OF YOGG-SARON SPELL_SHADOW_NOVA = 62714, + SPELL_SHADOW_NOVA_SARA = 65719, SPELL_DARK_VOLLEY = 63038, // SARA P2 @@ -290,16 +293,16 @@ const uint32 TABLE_GOSSIP_ENTRY[4] = {NPC_FREYA_GOSSIP, NPC_HODIR_GOSSIP, NPC_MI static LocationsXY yoggPortalLoc[] = { - {1970.48f, -9.75f, 325.5f}, - {1992.76f, -10.21f, 325.5f}, - {1995.53f, -39.78f, 325.5f}, - {1969.25f, -42.00f, 325.5f}, - {1960.62f, -32.00f, 325.5f}, - {1981.98f, -5.69f, 325.5f}, - {1982.78f, -45.73f, 325.5f}, - {2000.66f, -29.68f, 325.5f}, - {1999.88f, -19.61f, 325.5f}, - {1961.37f, -19.54f, 325.5f} + {1964.60f, -42.71f, 325.08f}, + {1986.94f, -46.21f, 324.98f}, + {1989.50f, -6.70f, 325.08f}, + {1965.52f, -8.09f, 324.95f}, + {2000.84f, -25.40f, 325.19f}, + {1960.22f, -26.14f, 325.01f}, + {1976.30f, -47.83f, 325.11f}, + {1997.69f, -37.46f, 325.04f}, + {1998.07f, -13.36f, 325.17f}, + {1976.99f, -3.96f, 325.17f} }; enum Texts @@ -452,6 +455,8 @@ struct boss_yoggsaron_sara : public ScriptedAI me->SetVisible(true); me->SetDisplayId(me->GetNativeDisplayId()); me->SetDisableGravity(true); + me->SetFaction(FACTION_FRIENDLY); + me->ClearUnitState(UNIT_STATE_EVADE); EnableSara(false); SpawnClouds(); @@ -492,8 +497,6 @@ struct boss_yoggsaron_sara : public ScriptedAI // Engage Keepers summons.DoZoneInCombat(); - me->CastSpell(me, SPELL_SANITY_BASE, true); - events.ScheduleEvent(EVENT_SARA_P1_DOORS_CLOSE, 15s, 0, EVENT_PHASE_ONE); events.ScheduleEvent(EVENT_SARA_P1_BERSERK, 15min, 0, 0); events.ScheduleEvent(EVENT_SARA_P1_SUMMON, 0ms, 0, EVENT_PHASE_ONE); @@ -579,15 +582,15 @@ struct boss_yoggsaron_sara : public ScriptedAI void AddPortals() { _summonSpeed -= 0.1f; - Creature* cr = nullptr; + Creature* creature = nullptr; // Spawn Portals for (uint8 i = 0; i < RAID_MODE(4, 10); ++i) { - if ((cr = me->SummonCreature(NPC_DESCEND_INTO_MADNESS, yoggPortalLoc[i].x, yoggPortalLoc[i].y, yoggPortalLoc[i].z, 0, TEMPSUMMON_TIMED_DESPAWN, 25000))) + if ((creature = me->SummonCreature(NPC_DESCEND_INTO_MADNESS, yoggPortalLoc[i].x, yoggPortalLoc[i].y, yoggPortalLoc[i].z, 0, TEMPSUMMON_TIMED_DESPAWN, 25000))) { - cr->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE | UNIT_FLAG_NON_ATTACKABLE); - cr->SetArmor(_currentIllusion); + creature->SetArmor(_currentIllusion); + creature->CastSpell(creature, SPELL_TELEPORT_PORTAL_VISUAL, true); } } @@ -699,39 +702,33 @@ struct boss_yoggsaron_sara : public ScriptedAI me->CastSpell(me, SPELL_SHATTERED_ILLUSION, true); } - void DamageTaken(Unit* who, uint32& damage, DamageEffectType, SpellSchoolMask) override + void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType /*damagetype*/, SpellSchoolMask /*damageSchoolMask*/) override { // Guardians can be spawned by walking into Ominous Clouds even when InitFight // never ran (e.g. Vezax not defeated); their novas must not start phase 2 then. - if (!_instance || _instance->GetBossState(BOSS_YOGGSARON) != IN_PROGRESS) + if (!_instance || _instance->GetBossState(BOSS_YOGGSARON) != IN_PROGRESS || !attacker || attacker->GetEntry() != NPC_GUARDIAN_OF_YS || _secondPhase) { damage = 0; return; } - if (who && who->GetEntry() == NPC_GUARDIAN_OF_YS && !_secondPhase) + // START PHASE 2 + if (me->GetHealth() <= damage) { - damage = 25000; + _secondPhase = true; + damage = 0; - // START PHASE 2 - if (me->GetHealth() <= damage) - { - _secondPhase = true; - damage = 0; + events.SetPhase(EVENT_PHASE_TWO); + me->SetHealth(me->GetMaxHealth()); + me->SetInCombatWithZone(); + me->SetFaction(FACTION_MONSTER_2); - events.SetPhase(EVENT_PHASE_TWO); - me->SetHealth(me->GetMaxHealth()); + if (Creature* creature = me->SummonCreature(NPC_YOGG_SARON, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), M_PI)) + creature->SetVisible(false); - if (Creature* cr = me->SummonCreature(NPC_YOGG_SARON, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), M_PI)) - cr->SetVisible(false); - - _p2TalkTimer++; - Talk(SAY_SARA_TRANSFORM_1); - } - return; + _p2TalkTimer++; + Talk(SAY_SARA_TRANSFORM_1); } - - damage = 0; } // Players who descend into a portal are teleported ~100 yd below to the illusion @@ -1040,7 +1037,8 @@ struct boss_yoggsaron_guardian_of_ys : public ScriptedAI void JustDied(Unit*) override { - me->CastSpell((Unit*)nullptr, SPELL_SHADOW_NOVA, true); + DoCastAOE(SPELL_SHADOW_NOVA, true); + DoCastAOE(SPELL_SHADOW_NOVA_SARA, true); } void UpdateAI(uint32 diff) override @@ -2101,7 +2099,8 @@ struct boss_yoggsaron_voice : public NullCreatureAI void Reset() override { - me->CastSpell(me, SPELL_INSANE_PERIODIC, true); + DoCastSelf(SPELL_INSANE_PERIODIC, true); + DoCastSelf(SPELL_SANITY_BASE, true); } void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override From cde7b196f44132f596ffde259a615168c31bb3a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:31:42 +0000 Subject: [PATCH 57/67] chore(DB): import pending files Referenced commit(s): 65443de1cd2f715ce1970a13244cdb44e924eef7 --- .../rev_1784123420947336369.sql => db_world/2026_07_15_03.sql} | 1 + .../rev_1784123661594956864.sql => db_world/2026_07_15_04.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1784123420947336369.sql => db_world/2026_07_15_03.sql} (88%) rename data/sql/updates/{pending_db_world/rev_1784123661594956864.sql => db_world/2026_07_15_04.sql} (95%) diff --git a/data/sql/updates/pending_db_world/rev_1784123420947336369.sql b/data/sql/updates/db_world/2026_07_15_03.sql similarity index 88% rename from data/sql/updates/pending_db_world/rev_1784123420947336369.sql rename to data/sql/updates/db_world/2026_07_15_03.sql index cf0e67ff7..c3a7d6396 100644 --- a/data/sql/updates/pending_db_world/rev_1784123420947336369.sql +++ b/data/sql/updates/db_world/2026_07_15_03.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_15_02 -> 2026_07_15_03 -- Yogg Portal 'Descend Into Madness' -- Movement flags: DisableGravity + Root DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 34072); diff --git a/data/sql/updates/pending_db_world/rev_1784123661594956864.sql b/data/sql/updates/db_world/2026_07_15_04.sql similarity index 95% rename from data/sql/updates/pending_db_world/rev_1784123661594956864.sql rename to data/sql/updates/db_world/2026_07_15_04.sql index 29a0a0f09..ee659d342 100644 --- a/data/sql/updates/pending_db_world/rev_1784123661594956864.sql +++ b/data/sql/updates/db_world/2026_07_15_04.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_15_03 -> 2026_07_15_04 -- Sara should start friendly, becoming hostile when Yogg-Saron reveals himself (phase 2) UPDATE `creature_template` SET `faction` = 35 WHERE `entry` IN (33134, 34332); From e89d2ba88717b721b7be8736eece69f448c58ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= <borzifrancesco@gmail.com> Date: Thu, 16 Jul 2026 09:56:04 +0200 Subject: [PATCH 58/67] fix(agents): make the project agent-agnostic (#26646) --- .coderabbit.yml | 6 +- .github/agents/README.md | 10 ++-- .github/agents/pr-reviewer.md | 8 +-- AGENTS.md | 100 +++++++++++++++++++++++++++++++++ CLAUDE.md | 101 +--------------------------------- 5 files changed, 113 insertions(+), 112 deletions(-) create mode 100644 AGENTS.md diff --git a/.coderabbit.yml b/.coderabbit.yml index 506590fb5..c3c15eafc 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -1,5 +1,5 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json -# CodeRabbit review config. Mirrors the hard rules in CLAUDE.md so the AI +# CodeRabbit review config. Mirrors the hard rules in AGENTS.md so the AI # reviewer enforces project conventions on every PR. language: en-US @@ -74,10 +74,10 @@ reviews: knowledge_base: code_guidelines: enabled: true - # CLAUDE.md is in CodeRabbit's defaults, but list it explicitly so the project's + # AGENTS.md is in CodeRabbit's defaults, but list it explicitly so the project's # full guideline doc is always pulled into review context. filePatterns: - - "CLAUDE.md" + - "AGENTS.md" chat: auto_reply: true diff --git a/.github/agents/README.md b/.github/agents/README.md index 488bd1026..2e7e7d58c 100644 --- a/.github/agents/README.md +++ b/.github/agents/README.md @@ -19,14 +19,14 @@ The PR Reviewer agent is configured to review pull requests with deep understand - Testing and validation requirements - Security and quality standards -**Key Feature**: This agent always references `/CLAUDE.md`, the C++ Code Standards wiki, and the SQL Standards wiki before reviewing any PR. +**Key Feature**: This agent always references `/AGENTS.md`, the C++ Code Standards wiki, and the SQL Standards wiki before reviewing any PR. ## How It Works When an AI tool reviews a PR in this repository: 1. The tool reads the appropriate agent configuration from this directory -2. The agent configuration instructs it to read `/CLAUDE.md` for full project context +2. The agent configuration instructs it to read `/AGENTS.md` for full project context 3. For C++ changes, the agent also references the [C++ Code Standards wiki](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) 4. For SQL changes, the agent also references the [SQL Standards wiki](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) 5. The agent applies project-specific rules during review @@ -36,7 +36,7 @@ When an AI tool reviews a PR in this repository: If you're using AI tools to assist with contributions: -1. Familiarize yourself with `/CLAUDE.md` - it contains essential project knowledge +1. Familiarize yourself with `/AGENTS.md` - it contains essential project knowledge 2. Review the [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) for C++ contributions 3. Review the [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) for database contributions 4. The AI agents will help ensure your changes follow project standards @@ -47,13 +47,13 @@ If you're using AI tools to assist with contributions: To update agent configurations: 1. Edit the relevant `.md` file in this directory -2. Ensure any changes align with documentation in `/CLAUDE.md` and the wiki standards +2. Ensure any changes align with documentation in `/AGENTS.md` and the wiki standards 3. Test the updated configuration with a sample PR review 4. Commit changes following the project's commit message format ## References -- Project guidelines: `/CLAUDE.md` +- Project guidelines: `/AGENTS.md` - C++ standards: [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) - SQL standards: [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) - PR template: `/pull_request_template.md` diff --git a/.github/agents/pr-reviewer.md b/.github/agents/pr-reviewer.md index c685744c7..878f6aa97 100644 --- a/.github/agents/pr-reviewer.md +++ b/.github/agents/pr-reviewer.md @@ -5,13 +5,13 @@ You are an expert code reviewer for the AzerothCore project. When reviewing pull ## Required Reading Before reviewing any PR, you MUST read and follow the instructions in: -- `/CLAUDE.md` - Contains comprehensive project architecture, coding standards, build instructions, and PR requirements +- `/AGENTS.md` - Contains comprehensive project architecture, coding standards, build instructions, and PR requirements - [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) - Detailed C++ coding conventions and style guide - [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) - SQL query formatting and database standards ## Key Review Focus Areas -Based on CLAUDE.md, always verify: +Based on AGENTS.md, always verify: ### 1. Commit Message Format - Uses Conventional Commits: `Type(Scope/Subscope): Short description` @@ -95,7 +95,7 @@ Based on CLAUDE.md, always verify: ## Review Process -1. Read CLAUDE.md to understand project context +1. Read AGENTS.md to understand project context 2. Review C++ Code Standards wiki for C++ changes 3. Review SQL Standards wiki for database changes 4. Check commit message format @@ -108,7 +108,7 @@ Based on CLAUDE.md, always verify: ## Feedback Style - Be constructive and educational -- Reference specific sections of CLAUDE.md, C++ Code Standards, or SQL Standards when applicable +- Reference specific sections of AGENTS.md, C++ Code Standards, or SQL Standards when applicable - Suggest specific fixes with code examples if necessary - Highlight both issues and good practices - For C++ issues, cite specific rule from C++ Code Standards wiki diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..02c62b97a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,100 @@ +# AGENTS.md + +AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK), built with CMake, backed by MySQL. + +## Agent rules + +- **Do not configure or build unless explicitly asked.** Builds are slow and rarely needed for code changes. +- **Never edit SQL files outside `data/sql/updates/pending_db_*/` unless explicitly requested. ** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable. + +## Build + +Out-of-source build is required (in-source is blocked). + +```bash +mkdir -p build && cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DSCRIPTS=static -DMODULES=static +make -j$(nproc) && make install +``` + +C++20 required (`CMAKE_CXX_STANDARD 20`). Useful flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. + +Tests (Google Test, in `src/test/`): configure `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. + +## Repository layout + +- `src/common/` — networking (Asio), crypto, config, logging, shared utilities. +- `src/server/game/` — core gameplay; compiled into worldserver. +- `src/server/scripts/` — content scripts grouped by region (`EasternKingdoms/`, `Northrend/`, …), class (`Spells/spell_mage.cpp`, …), and domain (`Commands/`, `Pet/`, `OutdoorPvP/`, `World/`). +- `src/server/database/` — DB abstraction and schema updater. +- `src/server/shared/` — code shared by auth and world servers. +- `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085). +- `src/test/` — Google Test unit tests + mocks. +- `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored). +- `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`. +- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts. +- `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored. +- `deps/` — vendored third-party dependencies. + +## Adding SQL updates + +1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`). +2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` to write into. +3. Conventions enforced by `apps/codestyle/codestyle-sql.py`: every `INSERT` preceded by a matching `DELETE` (idempotency); no double semicolons; no multiple blank lines; InnoDB engine. + +The three databases: + +- `acore_auth` — accounts, realm list, IP/account bans, session keys. Shared across all realms. +- `acore_characters` — per-character state: characters, inventory, in-progress quests, mail, guilds, arena teams, achievements. One per realm. +- `acore_world` — static game content: creature/gameobject/item/quest templates, spawn lists, loot tables, SmartAI scripts, gossip, conditions. Read-mostly; rebuilt from SQL. + +## Code style + +Formatting (charset, indent width, line length, final newline, trailing whitespace) follows `.editorconfig`. + +Run the linters before claiming a change is done: + +```bash +python apps/codestyle/codestyle-cpp.py # C++ +python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master) +``` + +Hard rules (also enforced by CI with `-Werror`, plus `cppcheck`): + +- 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 120 cols, trailing newline. +- Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`. +- `auto const&` (not `const auto&`); `Type const*` (not `const Type*`). +- Use `{}` format specifiers (`fmt`-style), not `%u`/`%s`. +- Use the typed helpers, not raw flag access: + - `IsPlayer()`, `IsCreature()`, `IsItem()`, … instead of `GetTypeId() == TYPEID_*`. + - `GetNpcFlags()`, `HasNpcFlag()`, `SetNpcFlag()`, `RemoveNpcFlag()`, `ReplaceAllNpcFlags()` instead of `*Flag(UNIT_NPC_FLAGS, …)`. + - `IsRefundable()`, `IsBOPTradable()`, `IsWrapped()` instead of `HasFlag(ITEM_FIELD_FLAGS, …)`. + - `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`. + - `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`. + +## Project conventions + +- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`/`ERROR`/`DEBUG`/`TRACE`). Categories are hierarchical, dot-separated (`server.loading`, `entities.player`, `sql.dev`). No `printf`-style, no `sLog->`, no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. +- **Random**: use helpers in `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Not `std::rand` or `<random>`. +- **Strings**: `Acore::StringFormat(fmt, args...)` (`{}` placeholders) — `src/common/Utilities/StringFormat.h`. +- **Config**: `sConfigMgr->GetOption<T>("Name", default)`. +- **Namespace**: project-wide `Acore::` (no `Trinity::` remnants — rename when porting from upstream forks). +- **Long-lived references**: don't store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `Map::GetCreature(guid)`, etc. +- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums), not raw query strings. Non-blocking reads go async: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback`). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. +- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation), both members of `CreatureAI` — don't roll your own tick counters. See any boss script under `src/server/scripts/`. + +## Scripting registration + +Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist: + +- **Spell / aura scripts**: `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) inside `AddSC_<name>()`. +- **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern. + +Then declare and call `AddSC_<name>()` from the regional loader (`Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, …). + +**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table, not C++ (engine: `src/server/game/AI/SmartScripts/`). For new creature behaviour prefer SmartAI (via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. + +**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro) inside `AddSC_<name>()`. Full list: https://www.azerothcore.org/wiki/hooks-script. + +Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored). diff --git a/CLAUDE.md b/CLAUDE.md index 0d4c0b76b..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,100 +1 @@ -# CLAUDE.md - -AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK), built with CMake, backed by MySQL. - -## Agent rules - -- **Do not configure or build unless explicitly asked.** Builds are slow and rarely needed for code changes. -- **Never edit SQL files outside `data/sql/updates/pending_db_*/` unless explicitly requested. ** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable. - -## Build - -Out-of-source build is required (in-source is blocked). - -```bash -mkdir -p build && cd build -cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DSCRIPTS=static -DMODULES=static -make -j$(nproc) && make install -``` - -C++20 required (`CMAKE_CXX_STANDARD 20`). Useful flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. - -Tests (Google Test, in `src/test/`): configure `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. - -## Repository layout - -- `src/common/` — networking (Asio), crypto, config, logging, shared utilities. -- `src/server/game/` — core gameplay; compiled into worldserver. -- `src/server/scripts/` — content scripts grouped by region (`EasternKingdoms/`, `Northrend/`, …), class (`Spells/spell_mage.cpp`, …), and domain (`Commands/`, `Pet/`, `OutdoorPvP/`, `World/`). -- `src/server/database/` — DB abstraction and schema updater. -- `src/server/shared/` — code shared by auth and world servers. -- `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085). -- `src/test/` — Google Test unit tests + mocks. -- `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored). -- `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`. -- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts. -- `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored. -- `deps/` — vendored third-party dependencies. - -## Adding SQL updates - -1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`). -2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` to write into. -3. Conventions enforced by `apps/codestyle/codestyle-sql.py`: every `INSERT` preceded by a matching `DELETE` (idempotency); no double semicolons; no multiple blank lines; InnoDB engine. - -The three databases: - -- `acore_auth` — accounts, realm list, IP/account bans, session keys. Shared across all realms. -- `acore_characters` — per-character state: characters, inventory, in-progress quests, mail, guilds, arena teams, achievements. One per realm. -- `acore_world` — static game content: creature/gameobject/item/quest templates, spawn lists, loot tables, SmartAI scripts, gossip, conditions. Read-mostly; rebuilt from SQL. - -## Code style - -Formatting (charset, indent width, line length, final newline, trailing whitespace) follows `.editorconfig`. - -Run the linters before claiming a change is done: - -```bash -python apps/codestyle/codestyle-cpp.py # C++ -python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master) -``` - -Hard rules (also enforced by CI with `-Werror`, plus `cppcheck`): - -- 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 120 cols, trailing newline. -- Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`. -- `auto const&` (not `const auto&`); `Type const*` (not `const Type*`). -- Use `{}` format specifiers (`fmt`-style), not `%u`/`%s`. -- Use the typed helpers, not raw flag access: - - `IsPlayer()`, `IsCreature()`, `IsItem()`, … instead of `GetTypeId() == TYPEID_*`. - - `GetNpcFlags()`, `HasNpcFlag()`, `SetNpcFlag()`, `RemoveNpcFlag()`, `ReplaceAllNpcFlags()` instead of `*Flag(UNIT_NPC_FLAGS, …)`. - - `IsRefundable()`, `IsBOPTradable()`, `IsWrapped()` instead of `HasFlag(ITEM_FIELD_FLAGS, …)`. - - `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`. - - `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`. - -## Project conventions - -- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`/`ERROR`/`DEBUG`/`TRACE`). Categories are hierarchical, dot-separated (`server.loading`, `entities.player`, `sql.dev`). No `printf`-style, no `sLog->`, no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. -- **Random**: use helpers in `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Not `std::rand` or `<random>`. -- **Strings**: `Acore::StringFormat(fmt, args...)` (`{}` placeholders) — `src/common/Utilities/StringFormat.h`. -- **Config**: `sConfigMgr->GetOption<T>("Name", default)`. -- **Namespace**: project-wide `Acore::` (no `Trinity::` remnants — rename when porting from upstream forks). -- **Long-lived references**: don't store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `Map::GetCreature(guid)`, etc. -- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums), not raw query strings. Non-blocking reads go async: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback`). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. -- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation), both members of `CreatureAI` — don't roll your own tick counters. See any boss script under `src/server/scripts/`. - -## Scripting registration - -Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist: - -- **Spell / aura scripts**: `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) inside `AddSC_<name>()`. -- **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern. - -Then declare and call `AddSC_<name>()` from the regional loader (`Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, …). - -**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table, not C++ (engine: `src/server/game/AI/SmartScripts/`). For new creature behaviour prefer SmartAI (via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. - -**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro) inside `AddSC_<name>()`. Full list: https://www.azerothcore.org/wiki/hooks-script. - -Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored). +@AGENTS.md From f3201056e0934dbef3ef04904982ec9fbfd9c80c Mon Sep 17 00:00:00 2001 From: sogladev <sogladev@gmail.com> Date: Thu, 16 Jul 2026 09:58:27 +0200 Subject: [PATCH 59/67] fix(Scripts/EoE): "You Don't Have an Eternity" timed achievement (#26636) --- .../scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp index ce575923c..89d474bb3 100644 --- a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp +++ b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp @@ -389,7 +389,10 @@ struct boss_malygos : public BossAI case EVENT_START_FIGHT: { instance->SetData(DATA_HIDE_IRIS_AND_PORTAL, 0); + + instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_YOU_DONT_HAVE_AN_ENTERNITY_EVENT); // just in case at reset some players already left the instance instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_YOU_DONT_HAVE_AN_ENTERNITY_EVENT); + me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_PACIFIED); if (Unit* target = me->SelectNearestTarget(250.0f)) { From 914119cccb4b47f994a1a9e65d3d8d6a0fc73c25 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:43:29 -0300 Subject: [PATCH 60/67] chore(Core): drop unused Language.h include in NPCHandler (#26650) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- src/server/game/Handlers/NPCHandler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/game/Handlers/NPCHandler.cpp b/src/server/game/Handlers/NPCHandler.cpp index d2c0383cc..62f117b9d 100644 --- a/src/server/game/Handlers/NPCHandler.cpp +++ b/src/server/game/Handlers/NPCHandler.cpp @@ -21,7 +21,6 @@ #include "Creature.h" #include "DatabaseEnv.h" #include "GameGraveyard.h" -#include "Language.h" #include "NPCPackets.h" #include "ObjectMgr.h" #include "Opcodes.h" From 689eb456f424b5f97c5acfeda2693dc2d25c621c Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:41:31 +0200 Subject: [PATCH 61/67] fix(CI/Windows): replace flaky chocolatey deps with direct provisioning (#26473) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- .github/workflows/windows_build.yml | 69 +++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index e579b89a7..3f8f576e9 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -37,12 +37,73 @@ jobs: - uses: actions/checkout@v4 - name: ccache uses: hendrikmuhs/ccache-action@v1.2.13 + # Cache the fetched Boost + MySQL client so we don't re-download ~350 MB + # and re-run the Boost installer on every run. The Test-Path guards in + # "Configure OS" short-circuit the install work when these are restored. + - name: Cache Boost & MySQL + uses: actions/cache@v4 + with: + path: | + C:\local\boost_1_87_0 + C:\tools\mysql\current + key: winbuild-deps:boost-1.87.0:mysql-8.4.9 - name: Configure OS - shell: bash - env: - CONTINUOUS_INTEGRATION: true + shell: pwsh run: | - ./acore.sh install-deps + $ErrorActionPreference = 'Stop' + + # The windows-latest runner image already ships CMake, Visual Studio + # (with the native desktop workload + MSVC) and OpenSSL, so we only need to + # provide Boost and the MySQL client libraries. We fetch these directly + # instead of via chocolatey, whose pinned packages keep breaking on CI + # (VS detection failures and slproweb removing old OpenSSL installers). + + # --- Boost 1.87.0 (msvc-14.3, 64-bit) prebuilt binaries -> C:\local\boost_1_87_0 (BOOST_ROOT) --- + # Use the direct-download host; the plain /download URL serves an HTML interstitial. + if (-not (Test-Path 'C:\local\boost_1_87_0\boost')) + { + $boostExe = Join-Path $env:RUNNER_TEMP 'boost.exe' + Invoke-WebRequest -Uri 'https://master.dl.sourceforge.net/project/boost/boost-binaries/1.87.0/boost_1_87_0-msvc-14.3-64.exe?viasf=1' -OutFile $boostExe + $boostMB = (Get-Item $boostExe).Length / 1MB + Write-Host ("Downloaded Boost installer: {0:N1} MB" -f $boostMB) + if ($boostMB -lt 50) { throw "Boost installer download looks wrong ($([int]$boostMB) MB) - likely an HTML page." } + $boostProc = Start-Process -FilePath $boostExe -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/DIR=C:\local\boost_1_87_0' -Wait -PassThru + if ($boostProc.ExitCode -ne 0) { throw "Boost installer failed with exit code $($boostProc.ExitCode)." } + # Fail fast if the installer "succeeded" but didn't lay down the expected tree, + # instead of surfacing a cryptic error later during CMake configuration. + if (-not (Test-Path 'C:\local\boost_1_87_0\boost\version.hpp')) { throw 'Boost install missing headers at C:\local\boost_1_87_0\boost.' } + if (-not (Test-Path 'C:\local\boost_1_87_0\lib64-msvc-14.3')) { throw 'Boost install missing prebuilt libs at C:\local\boost_1_87_0\lib64-msvc-14.3.' } + } + + # --- MySQL 8.4 client headers + libs -> C:\tools\mysql\current (path the build expects) --- + if (-not (Test-Path 'C:\tools\mysql\current\lib\mysqlclient.lib')) + { + # Use the archives CDN directly (keeps every version, unlike dev.mysql.com/get which only + # serves the current GA). A browser user-agent is required or Oracle returns an HTML error page. + $mysqlZip = Join-Path $env:RUNNER_TEMP 'mysql.zip' + Invoke-WebRequest -Uri 'https://cdn.mysql.com/archives/mysql-8.4/mysql-8.4.9-winx64.zip' -OutFile $mysqlZip -UserAgent 'Mozilla/5.0' + $mysqlMB = (Get-Item $mysqlZip).Length / 1MB + Write-Host ("Downloaded MySQL zip: {0:N1} MB" -f $mysqlMB) + if ($mysqlMB -lt 50) { throw "MySQL zip download looks wrong ($([int]$mysqlMB) MB)." } + New-Item -ItemType Directory -Force -Path 'C:\tools\mysql' | Out-Null + Expand-Archive -Path $mysqlZip -DestinationPath 'C:\tools\mysql' -Force + if (Test-Path 'C:\tools\mysql\current') { Remove-Item -Recurse -Force 'C:\tools\mysql\current' } + Rename-Item 'C:\tools\mysql\mysql-8.4.9-winx64' 'C:\tools\mysql\current' + # Fail fast if the archive layout changed and the client import library isn't + # where the build expects it, rather than failing later during CMake/link. + if (-not (Test-Path 'C:\tools\mysql\current\lib\mysqlclient.lib')) { throw 'MySQL client import library missing at C:\tools\mysql\current\lib\mysqlclient.lib.' } + } + + # --- OpenSSL: use the copy already installed on the runner image --- + $opensslRoot = @($env:OPENSSL_ROOT_DIR, 'C:\Program Files\OpenSSL', 'C:\Program Files\OpenSSL-Win64') | + Where-Object { $_ -and (Test-Path (Join-Path $_ 'include\openssl\opensslv.h')) } | + Select-Object -First 1 + if (-not $opensslRoot) + { + throw 'No OpenSSL development install found on the runner image.' + } + "OPENSSL_ROOT_DIR=$opensslRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Using OpenSSL at $opensslRoot" - name: Build shell: bash run: | From 3e3aa187242d8e4056a42df3861c97cff93115ed Mon Sep 17 00:00:00 2001 From: zackchadwick <73787423+zackchadwick@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:44:25 -0400 Subject: [PATCH 62/67] fix(DB/Quest): Washed Ashore typo (#26645) Co-authored-by: Kitzunu <24550914+Kitzunu@users.noreply.github.com> --- data/sql/updates/pending_db_world/rev_1784153969116784579.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784153969116784579.sql diff --git a/data/sql/updates/pending_db_world/rev_1784153969116784579.sql b/data/sql/updates/pending_db_world/rev_1784153969116784579.sql new file mode 100644 index 000000000..9c862fa54 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784153969116784579.sql @@ -0,0 +1 @@ +UPDATE `quest_offer_reward` SET `RewardText` = 'You''ve been a tremendous help here today; we now have a solid chance at unlocking the mystery of why these creatures choose to beach themselves on the coast of Darkshore. The idea that they might be fleeing the area around Teldrassil is disturbing. Should you come across others in your travels, please let me know. Our research here is but in its infancy.$B$BOn behalf of the Temple of the Moon, please accept this as thanks for the effort you have given. Thank you, $N!' WHERE `ID` = 4681; From 90805d298501f3b72dd8b3981e0165db468adc32 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:45:27 +0000 Subject: [PATCH 63/67] chore(DB): import pending files Referenced commit(s): 3e3aa187242d8e4056a42df3861c97cff93115ed --- .../rev_1784153969116784579.sql => db_world/2026_07_16_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784153969116784579.sql => db_world/2026_07_16_00.sql} (92%) diff --git a/data/sql/updates/pending_db_world/rev_1784153969116784579.sql b/data/sql/updates/db_world/2026_07_16_00.sql similarity index 92% rename from data/sql/updates/pending_db_world/rev_1784153969116784579.sql rename to data/sql/updates/db_world/2026_07_16_00.sql index 9c862fa54..5efb53892 100644 --- a/data/sql/updates/pending_db_world/rev_1784153969116784579.sql +++ b/data/sql/updates/db_world/2026_07_16_00.sql @@ -1 +1,2 @@ +-- DB update 2026_07_15_04 -> 2026_07_16_00 UPDATE `quest_offer_reward` SET `RewardText` = 'You''ve been a tremendous help here today; we now have a solid chance at unlocking the mystery of why these creatures choose to beach themselves on the coast of Darkshore. The idea that they might be fleeing the area around Teldrassil is disturbing. Should you come across others in your travels, please let me know. Our research here is but in its infancy.$B$BOn behalf of the Temple of the Moon, please accept this as thanks for the effort you have given. Thank you, $N!' WHERE `ID` = 4681; From 40b80ebe95b7ddbd046371103fe89bb107a1182d Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:55:47 -0300 Subject: [PATCH 64/67] fix(Scripts/Ulduar): make Mimiron's Aerial Command Unit chase players (#26625) Co-authored-by: Unholychick <lucas__jensen@hotmail.com> Co-authored-by: Chaouki Dhib <chaodhib@gmail.com> Co-authored-by: sirikfoll <sirikfoll@hotmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- .../rev_1784088692116509100.sql | 7 +++++ .../Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1784088692116509100.sql diff --git a/data/sql/updates/pending_db_world/rev_1784088692116509100.sql b/data/sql/updates/pending_db_world/rev_1784088692116509100.sql new file mode 100644 index 000000000..dd3c9caa9 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784088692116509100.sql @@ -0,0 +1,7 @@ +-- Aerial Command Unit: hover height and flags to match TDB (hover-based chase) +-- 256 = UNIT_FLAG_IMMUNE_TO_PC (cleared by script at phase start), 512 = CREATURE_FLAG_EXTRA_NO_MOVE_FLAGS_UPDATE +UPDATE `creature_template` SET `HoverHeight` = 15, `unit_flags` = `unit_flags`|256, `flags_extra` = `flags_extra`|512 WHERE `entry` IN (33670, 34109); + +-- Values ported from TrinityCore (TrinityCore/TrinityCore@7c13b383); zeroed combat reach +-- made the hovering ACU unreachable for Magnetic Core's 15y check +UPDATE `creature_model_info` SET `BoundingRadius` = 0.31, `CombatReach` = 5 WHERE `DisplayID` = 28979; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index 95644214b..54102ba97 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -264,7 +264,8 @@ enum Texts #define GetVX001() instance->GetCreature(DATA_MIMIRON_VX001) #define GetACU() instance->GetCreature(DATA_MIMIRON_ACU) -Position const ACUSummonPos = { 2742.6265f, 2568.0571f, 377.22076f, 0.0f }; /// @todo: replace with sniffed position +// Z is above hover height (15y) so that activating hover does not relocate the ACU upwards +Position const ACUSummonPos = { 2744.650f, 2569.460f, 380.0f, 3.141593f }; struct boss_mimiron : public BossAI { @@ -1513,7 +1514,8 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI _isDefeated = false; _events.Reset(); _summons.DespawnAll(); - me->SetCombatMovement(false); /// @todo: research ACU behaviour + me->SetHover(false); + me->SetDisableGravity(true); } void SetData(uint32 id, uint32 value) override @@ -1528,7 +1530,12 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI break; case 3: _phase = 3; + // Hover instead of disabled gravity: chase then keeps the ACU at + // HoverHeight above ground instead of dragging it to ground level. + me->SetDisableGravity(false); + me->SetHover(true); me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->SetImmuneToPC(false); DoZoneInCombat(); _events.Reset(); _events.ScheduleEvent(EVENT_SUMMON_BOMB_BOT, 15s); @@ -1560,14 +1567,24 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI me->CastStop(); me->AttackStop(); me->SetReactState(REACT_PASSIVE); - me->SetDisableGravity(false); + // Raw flag removal first: MoveFall lands at ground + hover height, which would + // keep the ACU airborne, and SetHover(false) would relocate it without a spline. + // SetHover(false) afterwards is relocation-free and tells the client to stop + // hovering, else it keeps floating the grounded unit back up + me->RemoveUnitMovementFlag(MOVEMENTFLAG_HOVER); me->GetMotionMaster()->MoveFall(); + me->SetHover(false); _events.DelayEvents(25s); break; case DO_ENABLE_AERIAL: + if (_isDefeated) + break; me->SetDisableGravity(true); - me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ() + 10.0f); + // 16y: above hover height (15y) so SetHover does not relocate it further up + me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ() + 16.0f); me->m_Events.AddEventAtOffset([&] { + me->SetDisableGravity(false); + me->SetHover(true); me->SetReactState(REACT_AGGRESSIVE); }, 2s); break; @@ -1599,6 +1616,7 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI me->InterruptNonMeleeSpells(false); me->RemoveAllAurasExceptType(SPELL_AURA_CONTROL_VEHICLE); + me->SetDisableGravity(true); me->GetMotionMaster()->MovePoint(0, 2744.65f, 2569.46f, 381.34f); if (Creature* c = GetMimiron()) From 448f07f06e6bba120cb263126bfae1ddd27893a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:56:50 +0000 Subject: [PATCH 65/67] chore(DB): import pending files Referenced commit(s): 40b80ebe95b7ddbd046371103fe89bb107a1182d --- .../rev_1784088692116509100.sql => db_world/2026_07_16_01.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784088692116509100.sql => db_world/2026_07_16_01.sql} (93%) diff --git a/data/sql/updates/pending_db_world/rev_1784088692116509100.sql b/data/sql/updates/db_world/2026_07_16_01.sql similarity index 93% rename from data/sql/updates/pending_db_world/rev_1784088692116509100.sql rename to data/sql/updates/db_world/2026_07_16_01.sql index dd3c9caa9..842fa39b4 100644 --- a/data/sql/updates/pending_db_world/rev_1784088692116509100.sql +++ b/data/sql/updates/db_world/2026_07_16_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_16_00 -> 2026_07_16_01 -- Aerial Command Unit: hover height and flags to match TDB (hover-based chase) -- 256 = UNIT_FLAG_IMMUNE_TO_PC (cleared by script at phase start), 512 = CREATURE_FLAG_EXTRA_NO_MOVE_FLAGS_UPDATE UPDATE `creature_template` SET `HoverHeight` = 15, `unit_flags` = `unit_flags`|256, `flags_extra` = `flags_extra`|512 WHERE `entry` IN (33670, 34109); From ea96cb4b4d772ed3a517c72ff8570e167976c9b4 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:45:27 +0200 Subject: [PATCH 66/67] fix(Core/Realms): show realms as offline when no worldserver is running (#26658) --- src/server/apps/authserver/Main.cpp | 4 ++++ .../database/Database/Implementation/LoginDatabase.cpp | 1 + src/server/database/Database/Implementation/LoginDatabase.h | 1 + src/server/game/World/World.cpp | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index 6d351336a..f5c3b80bc 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -129,6 +129,10 @@ int main(int argc, char** argv) std::shared_ptr<void> dbHandle(nullptr, [](void*) { StopDB(); }); + // Mark every realm offline on startup; each worldserver clears this flag for its own realm once it is ready. + // This prevents realms from appearing online in the realm list when no worldserver is actually running. + LoginDatabase.DirectExecute("UPDATE realmlist SET flag = flag | {}", REALM_FLAG_OFFLINE); + std::shared_ptr<Acore::Asio::IoContext> ioContext = std::make_shared<Acore::Asio::IoContext>(); // Get the list of realms for the server diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index a90e1bcd7..1629b7a69 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -91,6 +91,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_UPD_LAST_ATTEMPT_IP, "UPDATE account SET last_attempt_ip = ? WHERE username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_ONLINE, "UPDATE account SET online = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_UPTIME_PLAYERS, "UPDATE uptime SET uptime = ?, maxplayers = ? WHERE realmid = ? AND starttime = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_REALM_ONLINE, "UPDATE realmlist SET flag = flag & ~? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_OLD_LOGS, "DELETE FROM logs WHERE (time + ?) < ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_ACCOUNT_ACCESS, "DELETE FROM account_access WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_ACCOUNT_ACCESS_BY_REALM, "DELETE FROM account_access WHERE id = ? AND (RealmID = ? OR RealmID = -1)", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index 140166dda..a91f3dbe0 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -75,6 +75,7 @@ enum LoginDatabaseStatements : uint32 LOGIN_UPD_LAST_ATTEMPT_IP, LOGIN_UPD_ACCOUNT_ONLINE, LOGIN_UPD_UPTIME_PLAYERS, + LOGIN_UPD_REALM_ONLINE, LOGIN_DEL_OLD_LOGS, LOGIN_DEL_ACCOUNT_ACCESS, LOGIN_DEL_ACCOUNT_ACCESS_BY_REALM, diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index c12ff71b8..2c302ba05 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1286,6 +1286,12 @@ void World::Update(uint32 diff) stmt->SetData(2, realm.Id.Realm); stmt->SetData(3, uint32(GameTime::GetStartTime().count())); LoginDatabase.Execute(stmt); + + // Re-assert this realm as online in case the offline flag was set externally (e.g. an authserver restart). + LoginDatabasePreparedStatement* onlineStmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_REALM_ONLINE); + onlineStmt->SetData(0, uint8(REALM_FLAG_OFFLINE)); + onlineStmt->SetData(1, realm.Id.Realm); + LoginDatabase.Execute(onlineStmt); } ///- Process Game events when necessary From 0cb715b766319754574f2361e86fb1bed70ffc94 Mon Sep 17 00:00:00 2001 From: EricksOliveira <ericksoliveira258@gmail.com> Date: Fri, 17 Jul 2026 07:26:29 +0000 Subject: [PATCH 67/67] fix(Core/Unit): restore immediate combat engagement for non-controllable guardians (#26654) --- src/server/game/Entities/Unit/Unit.cpp | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 9b08f1c0a..b8beef5f0 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -7425,24 +7425,27 @@ bool Unit::Attack(Unit* victim, bool meleeAttack) else if (victim->IsCreature()) victim->ToCreature()->UpdateLeashExtensionTime(); - // set position before any AI calls/assistance - //if (IsCreature()) - // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); - // player-controlled creatures (pets, charms) enter combat on contact instead - // (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked) - if (creature && !IsControlledByPlayer()) + // Player-controlled creatures (pets, charms) enter combat on contact instead + // (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked). + // Non-controllable guardians (e.g. Shaman Elementals, Infernal) have no attack + // command and must engage immediately so their AI can chase and attack. + + if (creature && (!IsControlledByPlayer() || (IsGuardian() && !IsControllableGuardian()))) { EngageWithTarget(victim); - creature->SendAIReaction(AI_REACTION_HOSTILE); + if (!IsControlledByPlayer()) + { + creature->SendAIReaction(AI_REACTION_HOSTILE); - /// @todo: Implement aggro range, detection range and assistance range templates - if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE))) - creature->CallAssistance(); + /// @todo: Implement aggro range, detection range and assistance range templates + if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE))) + creature->CallAssistance(); - creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD)); + creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD)); - SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + } } // delay offhand weapon attack by 50% of the base attack time