From 9c80943df96e191c53dc5320d55f405bfd90d2e1 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:57:19 -0300 Subject: [PATCH 01/47] fix(Core/Unit): require contested guard attacker for friendly-target attack bypass (#26451) Co-authored-by: Claude Fable 5 --- src/server/game/Entities/Unit/Unit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 627e90f81..66f57773f 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -10835,7 +10835,7 @@ bool Unit::_IsValidAttackTarget(Unit const* target, SpellInfo const* bySpell, Wo if (Player const* player = target->GetCharmerOrOwnerPlayerOrPlayerItself()) isContestedPvp = player->HasPlayerFlag(PLAYER_FLAGS_CONTESTED_PVP); - if (!isContestedGuard && !isContestedPvp) + if (!isContestedGuard || !isContestedPvp) return false; } From acd519b9a508de2f6e9a5997aea1b82ad62ed6e3 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:49:52 -0300 Subject: [PATCH 02/47] chore(Core/Config): remove dead unreachable-target evade configs (#26460) Co-authored-by: Claude Fable 5 --- src/server/apps/worldserver/worldserver.conf.dist | 14 -------------- src/server/game/World/WorldConfig.cpp | 2 -- src/server/game/World/WorldConfig.h | 2 -- 3 files changed, 18 deletions(-) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 956972d3c..c688d98d1 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -3175,13 +3175,6 @@ Creature.MovingStopTimeForPlayer = 180000 WaypointMovementStopTimeForPlayer = 120 -# NpcEvadeIfTargetIsUnreachable -# Description: Specifies the time (in seconds) that a creature whom target -# is unreachable to end up in evade mode. -# Default: 5 - -NpcEvadeIfTargetIsUnreachable = 5 - # NpcRegenHPIfTargetIsUnreachable # Description: Regenerates HP for Creatures in Raids if they cannot reach the target. # Keep disabled if you are experiencing mmaps/pathing issues. @@ -3191,13 +3184,6 @@ NpcEvadeIfTargetIsUnreachable = 5 NpcRegenHPIfTargetIsUnreachable = 1 -# NpcRegenHPTimeIfTargetIsUnreachable -# Description: Specifies the time (in seconds) that a creature whom target -# is unreachable in raid to end up regenerate health. -# Default: 10 - -NpcRegenHPTimeIfTargetIsUnreachable = 10 - # Creatures.CustomIDs # Description: The list of custom creatures with gossip dialogues hardcoded in core, # divided by "," without spaces. diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 7f6b51e98..0a45ae06e 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -663,8 +663,6 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_PORTAL_CHECK_ILVL, "DungeonAccessRequirements.PortalAvgIlevelCheck", false); SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_LFG_DBC_LEVEL_OVERRIDE, "DungeonAccessRequirements.LFGLevelDBCOverride", false); SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_OPTIONAL_STRING_ID, "DungeonAccessRequirements.OptionalStringID", 0); - SetConfigValue(CONFIG_NPC_EVADE_IF_NOT_REACHABLE, "NpcEvadeIfTargetIsUnreachable", 5); - SetConfigValue(CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID, "NpcRegenHPTimeIfTargetIsUnreachable", 10); SetConfigValue(CONFIG_REGEN_HP_CANNOT_REACH_TARGET_IN_RAID, "NpcRegenHPIfTargetIsUnreachable", true); //Debug diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index bdeddd3a1..ce358389e 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -374,8 +374,6 @@ enum ServerConfigs CONFIG_GUILD_MEMBER_LIMIT, CONFIG_GM_LEVEL_CHANNEL_MODERATION, CONFIG_TOGGLE_XP_COST, - CONFIG_NPC_EVADE_IF_NOT_REACHABLE, - CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID, CONFIG_FFA_PVP_TIMER, CONFIG_OUTDOOR_PVP_CAPTURE_RATE, CONFIG_LOOT_NEED_BEFORE_GREED_ILVL_RESTRICTION, From b4f2e6f8f7f2a06b45d7beec8a6f32d07586bebb Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:10:51 -0300 Subject: [PATCH 03/47] fix(Core/Creature): stop mid-fight summons and proximity aggro from preferring totems/pets over players (#26457) Co-authored-by: Claude Fable 5 --- src/server/game/Entities/Creature/Creature.cpp | 5 +++++ src/server/game/Entities/Object/Object.cpp | 5 +++-- src/server/game/Grids/Notifiers/GridNotifiers.cpp | 13 +++++++++++++ src/server/game/Grids/Notifiers/GridNotifiers.h | 4 +++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 8d3612cbf..60acd2781 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -1925,6 +1925,11 @@ bool Creature::CanStartAttack(Unit const* who, bool force) const if (!_IsTargetAcceptable(who)) return false; + // Totems never pull proximity aggro; they are only attacked in response + // to threat they generate themselves (e.g. Searing Totem) + if (who->IsTotem()) + return false; + if (IsNeutralToAll() || !IsWithinDistInMap(who, GetAggroRange(who) + m_CombatDistance, true, false, false)) return false; } diff --git a/src/server/game/Entities/Object/Object.cpp b/src/server/game/Entities/Object/Object.cpp index 8a0eebffc..7ff045e0b 100644 --- a/src/server/game/Entities/Object/Object.cpp +++ b/src/server/game/Entities/Object/Object.cpp @@ -2337,8 +2337,9 @@ TempSummon* Map::SummonCreature(uint32 entry, Position const& pos, SummonPropert summon->InitSummon(); - // call MoveInLineOfSight for nearby creatures - Acore::AIRelocationNotifier notifier(*summon); + // call MoveInLineOfSight for nearby players and creatures; players are visited + // first (grid typelist order) so aggressive summons prefer them over pets/totems + Acore::AIRelocationNotifier notifier(*summon, true); Cell::VisitObjects(summon, notifier, GetVisibilityRange()); return summon; diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.cpp b/src/server/game/Grids/Notifiers/GridNotifiers.cpp index 28f87f286..e38c8030c 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.cpp +++ b/src/server/game/Grids/Notifiers/GridNotifiers.cpp @@ -186,6 +186,19 @@ void AIRelocationNotifier::Visit(CreatureMapType& m) } } +void AIRelocationNotifier::Visit(PlayerMapType& m) +{ + if (!includePlayers) + return; + + Creature* creature = i_unit.ToCreature(); + if (!creature || creature->IsMoveInLineOfSightStrictlyDisabled()) + return; + + for (PlayerMapType::iterator iter = m.begin(); iter != m.end(); ++iter) + CreatureUnitRelocationWorker(creature, iter->GetSource()); +} + // Uses visibility map void MessageDistDeliverer::Visit(VisiblePlayersMap const& m) { diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.h b/src/server/game/Grids/Notifiers/GridNotifiers.h index 8484eb3b5..1f078197f 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.h +++ b/src/server/game/Grids/Notifiers/GridNotifiers.h @@ -91,9 +91,11 @@ namespace Acore { Unit& i_unit; bool isCreature; - explicit AIRelocationNotifier(Unit& unit) : i_unit(unit), isCreature(unit.IsCreature()) {} + bool includePlayers; + explicit AIRelocationNotifier(Unit& unit, bool includePlayers = false) : i_unit(unit), isCreature(unit.IsCreature()), includePlayers(includePlayers) {} template void Visit(GridRefMgr&) {} void Visit(CreatureMapType&); + void Visit(PlayerMapType&); }; enum class TeamFilter From b6328fa9bb3275873ae6f831ac9cf58812477865 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:24:42 -0300 Subject: [PATCH 04/47] fix(DB/Spells): Honor Among Thieves proc must not engage the rogue (#26462) Co-authored-by: Claude Fable 5 --- .../sql/updates/pending_db_world/rev_1783178553569042800.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783178553569042800.sql diff --git a/data/sql/updates/pending_db_world/rev_1783178553569042800.sql b/data/sql/updates/pending_db_world/rev_1783178553569042800.sql new file mode 100644 index 000000000..374021875 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783178553569042800.sql @@ -0,0 +1,5 @@ +-- Honor Among Thieves triggered combo point (51699) is beneficial for the rogue: +-- mark it positive so it never pulls the rogue into combat with the targeted enemy +-- (SPELL_ATTR0_CU_POSITIVE_EFF0 | SPELL_ATTR0_CU_POSITIVE_EFF1) +DELETE FROM `spell_custom_attr` WHERE `spell_id` = 51699; +INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES (51699, 0x6000000); From f8a0c2f608eb9ff58d4f1d9e0afbc8ac0eecfb5f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 18:26:10 +0000 Subject: [PATCH 05/47] chore(DB): import pending files Referenced commit(s): b6328fa9bb3275873ae6f831ac9cf58812477865 --- .../rev_1783178553569042800.sql => db_world/2026_07_04_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783178553569042800.sql => db_world/2026_07_04_00.sql} (89%) diff --git a/data/sql/updates/pending_db_world/rev_1783178553569042800.sql b/data/sql/updates/db_world/2026_07_04_00.sql similarity index 89% rename from data/sql/updates/pending_db_world/rev_1783178553569042800.sql rename to data/sql/updates/db_world/2026_07_04_00.sql index 374021875..fbfe949c3 100644 --- a/data/sql/updates/pending_db_world/rev_1783178553569042800.sql +++ b/data/sql/updates/db_world/2026_07_04_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_03_03 -> 2026_07_04_00 -- Honor Among Thieves triggered combo point (51699) is beneficial for the rogue: -- mark it positive so it never pulls the rogue into combat with the targeted enemy -- (SPELL_ATTR0_CU_POSITIVE_EFF0 | SPELL_ATTR0_CU_POSITIVE_EFF1) From f76595cf806032e9231b354db733f2562200d909 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:53:10 -0300 Subject: [PATCH 06/47] feat(Core/Battlefield): defer server restart during Wintergrasp (#26461) Co-authored-by: Claude Opus 4.8 --- .../rev_1783176822547680700.sql | 4 ++ .../apps/worldserver/worldserver.conf.dist | 11 +++++ src/server/game/Miscellaneous/Language.h | 5 +- src/server/game/World/World.cpp | 46 +++++++++++++++++-- src/server/game/World/World.h | 1 + src/server/game/World/WorldConfig.cpp | 1 + src/server/game/World/WorldConfig.h | 1 + 7 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1783176822547680700.sql diff --git a/data/sql/updates/pending_db_world/rev_1783176822547680700.sql b/data/sql/updates/pending_db_world/rev_1783176822547680700.sql new file mode 100644 index 000000000..9766608c1 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783176822547680700.sql @@ -0,0 +1,4 @@ +-- +DELETE FROM `acore_string` WHERE `entry` = 35455; +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35455, 'A Wintergrasp battle is in progress. The scheduled server maintenance has been postponed.', '겨울손아귀 전투가 진행 중입니다. 예정된 서버 점검이 연기되었습니다.', 'Une bataille du Joug-d''hiver est en cours. La maintenance programmée du serveur a été reportée.', 'Eine Schlacht um Tausendwinter ist im Gange. Die geplante Serverwartung wurde verschoben.', '冬拥湖战斗正在进行中。计划中的服务器维护已被推迟。', '冬擁湖戰鬥正在進行中。排定的伺服器維護已延後。', 'Hay una batalla de Templo Helado en curso. El mantenimiento programado del servidor se ha pospuesto.', 'Hay una batalla de Templo Helado en curso. El mantenimiento programado del servidor se ha pospuesto.', 'Идёт битва за Ледяную Грудь. Плановое обслуживание сервера отложено.'); diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index c688d98d1..7b16a07cb 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -3734,6 +3734,17 @@ Wintergrasp.KickVoAPlayers = 1 Wintergrasp.EssenceBothFactions = 0 +# +# Wintergrasp.DeferShutdownTimer +# Description: Defer a scheduled server shutdown/restart if a Wintergrasp battle +# is still in progress when the countdown elapses. It is rescheduled +# to the battle's remaining time plus this many minutes of buffer. +# Idle shutdowns are not affected. +# Default: 0 - (Disabled) +# N - (Enabled, defer to remaining battle time + N minutes) + +Wintergrasp.DeferShutdownTimer = 0 + # ################################################################################################### diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 20bd52792..a3cf7b59d 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1517,6 +1517,9 @@ enum AcoreStrings // Pet rename command LANG_PET_RENAME_INVALID = 35453, - LANG_PET_RENAME_SUCCESS = 35454 + LANG_PET_RENAME_SUCCESS = 35454, + + // Wintergrasp shutdown deferral + LANG_WG_SHUTDOWN_DEFERRED = 35455 }; #endif diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 9c2147dc2..c416a6080 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -56,6 +56,7 @@ #include "InstanceSaveMgr.h" #include "ItemEnchantmentMgr.h" #include "LFGMgr.h" +#include "Language.h" #include "Log.h" #include "LootItemStorage.h" #include "LootMgr.h" @@ -1450,10 +1451,15 @@ void World::_UpdateGameTime() ///- ... and it is overdue, stop the world (set m_stopEvent) if (_shutdownTimer <= elapsed.count()) { - if (!(_shutdownMask & SHUTDOWN_MASK_IDLE) || sWorldSessionMgr->GetActiveAndQueuedSessionCount() == 0) - _stopEvent = true; // exist code already set - else - _shutdownTimer = 1; // minimum timer value to wait idle state + ///- ... unless a Wintergrasp battle is running and deferral is enabled, in which case the + /// shutdown/restart is pushed past the end of the current battle and the world keeps running + if (!RescheduleShutdownForWintergrasp()) + { + if (!(_shutdownMask & SHUTDOWN_MASK_IDLE) || sWorldSessionMgr->GetActiveAndQueuedSessionCount() == 0) + _stopEvent = true; // exist code already set + else + _shutdownTimer = 1; // minimum timer value to wait idle state + } } ///- ... else decrease it and if necessary display a shutdown countdown to the users else @@ -1465,6 +1471,38 @@ void World::_UpdateGameTime() } } +/// Defer a pending shutdown/restart if a Wintergrasp battle is currently running. +/// Returns true when the shutdown timer was extended (world should keep running). +bool World::RescheduleShutdownForWintergrasp() +{ + uint32 const bufferMinutes = getIntConfig(CONFIG_WINTERGRASP_DEFER_SHUTDOWN); + if (!bufferMinutes) + return false; + + // Idle shutdowns wait for an empty server anyway; don't interfere with them + if (_shutdownMask & SHUTDOWN_MASK_IDLE) + return false; + + Battlefield* wg = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG); + if (!wg || !wg->IsEnabled() || !wg->IsWarTime()) + return false; + + // GetTimer() is the battle time remaining in milliseconds + _shutdownTimer = wg->GetTimer() / IN_MILLISECONDS + bufferMinutes * MINUTE; + + LOG_INFO("server.worldserver", "Server {} deferred: Wintergrasp battle in progress, rescheduled in {}", + (_shutdownMask & SHUTDOWN_MASK_RESTART ? "restart" : "shutdown"), secsToTimeString(_shutdownTimer)); + + sWorldSessionMgr->DoForAllOnlinePlayers([](Player* player) + { + LocaleConstant locale = player->GetSession()->GetSessionDbLocaleIndex(); + sWorldSessionMgr->SendServerMessage(SERVER_MSG_STRING, sObjectMgr->GetAcoreString(LANG_WG_SHUTDOWN_DEFERRED, locale), player); + }); + + ShutdownMsg(true); + return true; +} + /// Shutdown the server void World::ShutdownServ(uint32 time, uint32 options, uint8 exitcode, std::string const& reason) { diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index b0499a741..0f2eb0e80 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -242,6 +242,7 @@ public: protected: void _UpdateGameTime(); + bool RescheduleShutdownForWintergrasp(); // callback for UpdateRealmCharacters void _UpdateRealmCharCount(PreparedQueryResult resultCharCount,uint32 accountId); diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 0a45ae06e..1cc9cd997 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -613,6 +613,7 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, "Wintergrasp.SkipBattleSessionCount", 3500); SetConfigValue(CONFIG_WINTERGRASP_KICK_VOA_PLAYERS, "Wintergrasp.KickVoAPlayers", true, ConfigValueCache::Reloadable::No); SetConfigValue(CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS, "Wintergrasp.EssenceBothFactions", false); + SetConfigValue(CONFIG_WINTERGRASP_DEFER_SHUTDOWN, "Wintergrasp.DeferShutdownTimer", 0); SetConfigValue(CONFIG_BIRTHDAY_TIME, "BirthdayTime", 1222964635); SetConfigValue(CONFIG_MINIGOB_MANABONK, "Minigob.Manabonk.Enable", true); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index ce358389e..dd0707fce 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -332,6 +332,7 @@ enum ServerConfigs CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, CONFIG_WINTERGRASP_KICK_VOA_PLAYERS, CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS, + CONFIG_WINTERGRASP_DEFER_SHUTDOWN, CONFIG_PACKET_SPOOF_BANMODE, CONFIG_PACKET_SPOOF_BANDURATION, CONFIG_WARDEN_CLIENT_RESPONSE_DELAY, From ad5b63ee02b66d073764380875b8fd18ffa28514 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 18:56:40 +0000 Subject: [PATCH 07/47] chore(DB): import pending files Referenced commit(s): f76595cf806032e9231b354db733f2562200d909 --- .../rev_1783176822547680700.sql => db_world/2026_07_04_01.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783176822547680700.sql => db_world/2026_07_04_01.sql} (96%) diff --git a/data/sql/updates/pending_db_world/rev_1783176822547680700.sql b/data/sql/updates/db_world/2026_07_04_01.sql similarity index 96% rename from data/sql/updates/pending_db_world/rev_1783176822547680700.sql rename to data/sql/updates/db_world/2026_07_04_01.sql index 9766608c1..2c5aa87bd 100644 --- a/data/sql/updates/pending_db_world/rev_1783176822547680700.sql +++ b/data/sql/updates/db_world/2026_07_04_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_00 -> 2026_07_04_01 -- DELETE FROM `acore_string` WHERE `entry` = 35455; INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES From 0dd3fcd7c19d59e8a3c31ccf08192fe74540a7ab Mon Sep 17 00:00:00 2001 From: Rocco Silipo <108557877+Rorschach91@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:30:55 +0200 Subject: [PATCH 08/47] fix(DB/Script): Improve Citizen of Havenshire behavior. (#26467) --- .../pending_db_world/rev_1783179700.sql | 65 +++++++++++++++++++ .../ScarletEnclave/chapter1.cpp | 18 +++++ 2 files changed, 83 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783179700.sql diff --git a/data/sql/updates/pending_db_world/rev_1783179700.sql b/data/sql/updates/pending_db_world/rev_1783179700.sql new file mode 100644 index 000000000..d43c6f21c --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783179700.sql @@ -0,0 +1,65 @@ + +-- Register Rain of Darkness Spell Script. +DELETE FROM `spell_script_names` WHERE `spell_id` = 51761; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(51761, 'spell_q12641_rain_of_darkness'); + +-- Delete Personal SAI guid row (Pause Movement and Movement Resume). +DELETE FROM `smart_scripts` WHERE (`source_type` = 0) AND (`id` IN (9, 10)) AND (`entryorguid` IN (-128958, -128959, -128960, -128961, -128962, -128963, -128964, -128965, -128966, -128967, -128968, -128970, -128973, -128976, -128978, -128979, -128980, -128981, -128986, -128991, -128992, -128993, -128910, -128911, -128912, -128913, -128914, -128915, -128916, -128917, -128918, -128919, -128920, -128922, -128924, -128926, -128927, -128928, -128929, -128930, -128934, -128935, -128948, -128954)); + +-- Remove Rain of Darkness Dummies from map. +DELETE FROM `creature` WHERE `id` = 28643; + +-- Set Rain of Darkness Dummy Movement Flags. +DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 28643); +INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES +(28643, 0, 0, 1, 1, 0, 0, 0); + +-- Set Rain of Darkness Dummy SAI. +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 28643; + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 28643); +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 +(28643, 0, 0, 0, 54, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 52149, 2, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 'Rain of Darkness Dummy - On Just Summoned - Cast \'Rain of Darkness\''); + +-- Update Citizen of Havenshire Action Lists. +DELETE FROM `smart_scripts` WHERE (`source_type` = 9) AND (`entryorguid` IN (2857600, 2857700)); +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 +(2857600, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 51604, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Cast \'Serverside - Stun Self\''), +(2857600, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Emote 5'), +(2857600, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 4, 14561, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Sound 14561'), +(2857600, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Say Line 1'), +(2857600, 9, 4, 0, 0, 0, 100, 0, 2000, 2000, 0, 0, 0, 0, 17, 431, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Set Emote State 431'), +(2857700, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 51604, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Cast \'Serverside - Stun Self\''), +(2857700, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Emote 5'), +(2857700, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 4, 14564, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Sound 14564'), +(2857700, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Say Line 1'), +(2857700, 9, 4, 0, 0, 0, 100, 0, 2000, 2000, 0, 0, 0, 0, 17, 431, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Set Emote State 431'); + +-- Update Citizen of Havenshire SAI. +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE (`entry` IN (28576, 28577)); + +DELETE FROM `smart_scripts` WHERE (`entryorguid` IN (28576, 28577)) AND (`source_type` = 0) AND (`id` IN (5, 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 +(28576, 0, 5, 0, 1, 0, 30, 0, 5000, 20000, 5000, 20000, 0, 0, 11, 51761, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Out of Combat - Cast \'Rain of Darkness\''), +(28576, 0, 9, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - On Aggro - Set Reactstate Aggressive'), +(28577, 0, 5, 0, 1, 0, 30, 0, 5000, 20000, 5000, 20000, 0, 0, 11, 51761, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Out of Combat - Cast \'Rain of Darkness\''), +(28577, 0, 9, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - On Aggro - Set Reactstate Aggressive'); + +-- Remove old condition (it wasn't used in any case). +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 9) AND (`SourceEntry` = 28576) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 14) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 12678) AND (`ConditionValue2` = 0) AND (`ConditionValue3` = 0); + +-- Add Conditions to trigger Citizen of Havenshire Smart Events. +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 3) AND (`SourceEntry` IN (28576, 28577)) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 47) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 12678) AND (`ConditionValue2` = 10) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(22, 3, 28576, 0, 0, 47, 0, 12678, 10, 0, 0, 0, 0, '', 'Smart Event only occurs if player has quest 12678 in progress or completed.'), +(22, 3, 28577, 0, 0, 47, 0, 12678, 10, 0, 0, 0, 0, '', 'Smart Event only occurs if player has quest 12678 in progress or completed.'); + +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 10) AND (`SourceEntry` IN (28576, 28577)) AND (`SourceId` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(22, 10, 28576, 0, 1, 32, 0, 16, 0, 0, 0, 0, 0, '', 'Smart Event starts it Invoker is a Player.'), +(22, 10, 28576, 0, 1, 47, 0, 12678, 65, 0, 0, 0, 0, '', 'Smart Event starts if Player doesn\'t have quest 12678 in its quest log.'), +(22, 10, 28577, 0, 1, 32, 0, 16, 0, 0, 0, 0, 0, '', 'Smart Event starts it Invoker is a Player.'), +(22, 10, 28577, 0, 1, 47, 0, 12678, 65, 0, 0, 0, 0, '', 'Smart Event starts if Player doesn\'t have quest 12678 in its quest log.'), +(22, 10, 28576, 0, 2, 32, 0, 16, 0, 0, 1, 0, 0, '', 'Smart Event starts if Invoker is NOT a Player.'), +(22, 10, 28577, 0, 2, 32, 0, 16, 0, 0, 1, 0, 0, '', 'Smart Event starts if Invoker is NOT a Player.'); diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp index bcc48a812..d22350896 100644 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp +++ b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp @@ -101,6 +101,23 @@ class spell_q12641_death_comes_from_on_high_recall_eye : public SpellScript } }; +// 51761 - Rain of Darkness +class spell_q12641_rain_of_darkness : public SpellScript +{ + PrepareSpellScript(spell_q12641_rain_of_darkness); + + void ModDestHeight(SpellDestination& dest) + { + Position const offset = { 0.0f, 0.0f, 15.0f, 0.0f }; + dest.RelocateOffset(offset); + } + + void Register() override + { + OnDestinationTargetSelect += SpellDestinationTargetSelectFn(spell_q12641_rain_of_darkness::ModDestHeight, EFFECT_0, TARGET_DEST_CASTER_BACK); + } +}; + enum GiftOfTheHarvester { NPC_GHOUL = 28845, @@ -449,6 +466,7 @@ void AddSC_the_scarlet_enclave_c1() { RegisterSpellScript(spell_q12641_death_comes_from_on_high_summon_ghouls); RegisterSpellScript(spell_q12641_death_comes_from_on_high_recall_eye); + RegisterSpellScript(spell_q12641_rain_of_darkness); RegisterSpellScript(spell_item_gift_of_the_harvester); RegisterSpellScript(spell_q12698_the_gift_that_keeps_on_giving); new npc_scarlet_ghoul(); From b42ffa5cdd0c8c10b650f5b1f25ca27c815eed8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 19:32:10 +0000 Subject: [PATCH 09/47] chore(DB): import pending files Referenced commit(s): 0dd3fcd7c19d59e8a3c31ccf08192fe74540a7ab --- .../rev_1783179700.sql => db_world/2026_07_04_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783179700.sql => db_world/2026_07_04_02.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1783179700.sql b/data/sql/updates/db_world/2026_07_04_02.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1783179700.sql rename to data/sql/updates/db_world/2026_07_04_02.sql index d43c6f21c..05555ba75 100644 --- a/data/sql/updates/pending_db_world/rev_1783179700.sql +++ b/data/sql/updates/db_world/2026_07_04_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_01 -> 2026_07_04_02 -- Register Rain of Darkness Spell Script. DELETE FROM `spell_script_names` WHERE `spell_id` = 51761; From 7e8ebca4a1aaf42fa3afd9b7b54d1c8fbc830ca3 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:40:13 -0300 Subject: [PATCH 10/47] fix(Scripts/Ulduar): Add Mimiron elevator knockback (#26450) Co-authored-by: Unholychick Co-authored-by: Claude Opus 4.8 --- src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index 0eba106d7..87aace459 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -47,6 +47,9 @@ enum SpellData SPELL_MINE_EXPLOSION = 66351, SPELL_SUMMON_PROXIMITY_MINE = 65347, + // PHASE 1 -> 2 TRANSITION: + SPELL_ELEVATOR_KNOCKBACK = 65096, // Self-cast by the world trigger; sweeps players off the elevator as it rises + // PHASE 2: SPELL_HEAT_WAVE = 64533, @@ -98,6 +101,7 @@ enum NPCs NPC_ASSAULT_BOT = 34057, NPC_JUNK_BOT = 33855, NPC_MAGNETIC_CORE = 34068, + NPC_WORLD_TRIGGER = 21252, }; enum GOs @@ -488,6 +492,8 @@ struct boss_mimiron : public BossAI elevator->SetLootState(GO_READY); elevator->UseDoorOrButton(0, false); elevator->EnableCollision(false); + if (Creature* trigger = me->SummonCreature(NPC_WORLD_TRIGGER, elevator->GetPositionX(), elevator->GetPositionY(), elevator->GetPositionZ(), 0.0f, TEMPSUMMON_TIMED_DESPAWN, 5000)) + trigger->CastSpell(trigger, SPELL_ELEVATOR_KNOCKBACK); } events.ScheduleEvent(EVENT_ELEVATOR_INTERVAL_1, 6s); break; From 157ec27d858b6f56852bc31fe758c980b25cec88 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:41:45 -0300 Subject: [PATCH 11/47] fix(Core/Spells): allow Freya's Potent Pheromones to affect player pets (#26448) Co-authored-by: Claude Fable 5 --- src/server/game/Spells/SpellInfoCorrections.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index ea20e8202..f5e22058c 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -2016,7 +2016,6 @@ void SpellMgr::LoadSpellInfoCorrections() // Potent Pheromones ApplySpellFix({ 64321 }, [](SpellInfo* spellInfo) { - spellInfo->AttributesEx3 |= SPELL_ATTR3_ONLY_ON_PLAYER; spellInfo->AttributesEx |= SPELL_ATTR1_IMMUNITY_PURGES_EFFECT; }); From 14d3c78b4c1f1ca9b26a92f1ad23b379a6fe56ba Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:42:45 -0300 Subject: [PATCH 12/47] fix(Scripts/Ulduar): Brundir no longer despawns during Lightning Tendrils (#26449) Co-authored-by: Claude Opus 4.8 --- .../Ulduar/Ulduar/boss_assembly_of_iron.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp index 7e2eb3f7a..3fcd245cb 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp @@ -783,11 +783,13 @@ struct boss_stormcaller_brundir : public ScriptedAI me->SetDisableGravity(true); me->SetHover(true); - me->CombatStop(); + // AttackStop (not CombatStop) so he stays in combat while REACT_PASSIVE; + // otherwise UpdateVictim() sees engaged+passive+out-of-combat and evades + // him mid-flight, resetting the whole encounter. + me->AttackStop(); me->StopMoving(); me->SetReactState(REACT_PASSIVE); me->SetGuidValue(UNIT_FIELD_TARGET, ObjectGuid::Empty); - me->SetUnitFlag(UNIT_FLAG_STUNNED); me->CastSpell(me, SPELL_LIGHTNING_TENDRILS, true); me->CastSpell(me, SPELL_LIGHTNING_TENDRILS_2, true); @@ -807,16 +809,17 @@ struct boss_stormcaller_brundir : public ScriptedAI me->SetHover(false); me->SetReactState(REACT_AGGRESSIVE); me->SetDisableGravity(false); - if (Unit* flyTarget = ObjectAccessor::GetUnit(*me, _flyTargetGUID)) - { - me->Attack(flyTarget, false); - } - me->SetRegeneratingHealth(true); - _flyTargetGUID.Clear(); me->RemoveAura(sSpellMgr->GetSpellIdForDifficulty(SPELL_LIGHTNING_TENDRILS, me)); me->RemoveAura(SPELL_LIGHTNING_TENDRILS_2); DoResetThreatList(); + + // AttackStart (not Attack) so MoveChase is re-issued; Attack() alone only + // sets the victim, leaving him landed but standing still. + if (Unit* flyTarget = ObjectAccessor::GetUnit(*me, _flyTargetGUID)) + AttackStart(flyTarget); + + _flyTargetGUID.Clear(); events.CancelEvent(EVENT_LIGHTNING_FLIGHT); break; case EVENT_ENRAGE: From 3776f464e5fc16bf363e2f28931b501648f1956f Mon Sep 17 00:00:00 2001 From: sogladev Date: Sat, 4 Jul 2026 22:44:34 +0200 Subject: [PATCH 13/47] fix(Scripts/Ulduar): rework Flame Leviathan Overload sequence (#26360) --- .../rev_1782492883671113125.sql | 29 ++ src/server/game/Entities/Unit/Unit.cpp | 1 - .../Ulduar/Ulduar/boss_flame_leviathan.cpp | 262 ++++++++++-------- 3 files changed, 177 insertions(+), 115 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1782492883671113125.sql diff --git a/data/sql/updates/pending_db_world/rev_1782492883671113125.sql b/data/sql/updates/pending_db_world/rev_1782492883671113125.sql new file mode 100644 index 000000000..0e3d3bc44 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1782492883671113125.sql @@ -0,0 +1,29 @@ +-- +-- Flame Leviathan: Remove spell_linked_spell entries for SPELL_SYSTEMS_SHUTDOWN (62475). +-- These effects are now handled explicitly via script +DELETE FROM `spell_linked_spell` WHERE `spell_trigger` = -62475; + +-- 62399 Overload Circuit: Entry 33139 (cannon), Entry 33113 (boss) +UPDATE `conditions` SET `SourceGroup` = 1, `Comment` = 'target must be Flame Leviathan' WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 3) AND (`SourceEntry` = 62399) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33113) AND (`ConditionValue3` = 0); + +-- change turret (33142) summontype from CORPSE_TIMED_DESPAWN (6) to CORPSE_DESPAWN (5) so InstallAccessory skips SetDeathState +UPDATE `vehicle_template_accessory` SET `summontype` = 5 WHERE `entry` = 33114 AND `accessory_entry` = 33142; +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 2) AND (`SourceEntry` = 62399) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33139) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 2, 62399, 0, 0, 31, 0, 3, 33139, 0, 0, 0, 0, '', 'target must be Flame Leviathan'); + +-- ID - 62323 Hookshot +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 62323) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33114) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 62323, 0, 0, 31, 0, 3, 33114, 0, 0, 0, 0, '', 'must be Flame Leviathan Seat'); + +-- Add missing text +DELETE FROM `creature_text` WHERE (`CreatureID` = 33113) AND (`GroupID` IN (20)); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(33113, 20, 0, 'The Flame Leviathan begins to overload!', 41, 0, 100, 0, 0, 0, 33276, 0, 'Flame Leviathan EMOTE_OVERLOAD_START'); + +DELETE FROM `spell_script_names` WHERE `spell_id` = 62336; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(62336, 'spell_hookshot_aura'); + +DELETE FROM `spell_script_names` WHERE `ScriptName` = 'spell_vehicle_circuit_overload_aura'; diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 66f57773f..73219a924 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -15849,7 +15849,6 @@ void Unit::_ExitVehicle(Position const* exitPosition) init.Launch(); DisableSpline(); KnockbackFrom(pos.GetPositionX(), pos.GetPositionY(), 10.0f, 20.0f); - CastSpell(this, VEHICLE_SPELL_PARACHUTE, true); } // xinef: move fall, should we support all creatures that exited vehicle in air? Currently Quest Drag and Drop only, Air Assault quest diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp index 7193e43e1..6ed905267 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp @@ -56,6 +56,7 @@ enum LeviathanSpells // Shutdown spells SPELL_SYSTEMS_SHUTDOWN = 62475, SPELL_OVERLOAD_CIRCUIT = 62399, + SPELL_START_THE_ENGINE = 62472, // hard mode SPELL_TOWER_OF_STORMS = 65076, @@ -84,6 +85,10 @@ enum LeviathanSpells SPELL_LIQUID_PYRITE = 62494, SPELL_DUSTY_EXPLOSION = 63360, SPELL_DUST_CLOUD_IMPACT = 54740, + + // Hookshot + SPELL_HOOKSHOT_AURA = 62336, + SPELL_HOOKSHOT = 62323, }; enum GosNpcs @@ -115,12 +120,12 @@ enum Events EVENT_MISSILE = 2, EVENT_VENT = 3, EVENT_SPEED = 4, - EVENT_REINSTALL = 5, EVENT_HODIRS_FURY = 6, EVENT_FREYA = 7, EVENT_MIMIRONS_INFERNO = 8, EVENT_THORIMS_HAMMER = 9, EVENT_SOUND_BEGINNING = 10, + EVENT_EJECT_PLAYERS = 11, }; enum Texts @@ -145,6 +150,7 @@ enum Texts FLAME_LEVIATHAN_EMOTE_NATURE = 17, FLAME_LEVIATHAN_EMOTE_STORM = 18, FLAME_LEVIATHAN_EMOTE_REACTIVATE = 19, + FLAME_LEVIATHAN_EMOTE_OVERLOAD_START = 20, // NPC_BRANN_RADIO BRANN_RADIO_SAY_FL_START_0 = 0, @@ -179,7 +185,6 @@ enum Misc ACTION_START_BRANN_EVENT = 3, ACTION_DESPAWN_ADDS = 4, ACTION_DELAY_CANNON = 5, - ACTION_DESTROYED_TURRET = 6, }; const Position homePos = {322.39f, -14.5f, 409.8f, 3.14f}; @@ -197,7 +202,7 @@ struct boss_flame_leviathan : public BossAI uint32 _speakTimer; uint8 _towersCount; bool _shutdown; - uint32 _destroyedTurretCount; + uint8 _overloadCircuitCount; // Custom void BindPlayers(); @@ -284,7 +289,7 @@ struct boss_flame_leviathan : public BossAI _startTimer = 1; _speakTimer = 0; _towersCount = 0; - _destroyedTurretCount = 0; + _overloadCircuitCount = 0; if (instance->GetBossState(BOSS_LEVIATHAN) != SPECIAL) { @@ -402,13 +407,6 @@ struct boss_flame_leviathan : public BossAI else Talk(FLAME_LEVIATHAN_SAY_TOWER_NONE); return; - case EVENT_REINSTALL: - for (uint8 i = RAID_MODE(0, 2); i < 4; ++i) - if (Unit* seat = vehicle->GetPassenger(i)) - if (seat->IsCreature()) - seat->ToCreature()->AI()->EnterEvadeMode(); - Talk(FLAME_LEVIATHAN_EMOTE_REACTIVATE); - return; case EVENT_THORIMS_HAMMER: SummonTowerHelpers(TOWER_OF_STORMS); events.Repeat(1min, 2min); @@ -430,6 +428,13 @@ struct boss_flame_leviathan : public BossAI Talk(FLAME_LEVIATHAN_EMOTE_FROST); Talk(FLAME_LEVIATHAN_SAY_TOWER_FROST); return; + case EVENT_EJECT_PLAYERS: + for (int8 i = 0; i < 4; ++i) + if (Unit* seat = vehicle->GetPassenger(i)) + if (Vehicle* seatVehicle = seat->GetVehicleKit()) + if (Unit* player = seatVehicle->GetPassenger(SEAT_PLAYER)) + player->ExitVehicle(); + return; } if (me->isAttackReady() && !me->HasUnitState(UNIT_STATE_STUNNED)) @@ -441,20 +446,6 @@ struct boss_flame_leviathan : public BossAI } } } - - void DoAction(int32 action) override - { - if (action == ACTION_DESTROYED_TURRET) - { - ++_destroyedTurretCount; - - if (_destroyedTurretCount == RAID_MODE(2, 4)) - { - _destroyedTurretCount = 0; - me->CastSpell(me, SPELL_SYSTEMS_SHUTDOWN, true); - } - } - } }; void boss_flame_leviathan::BindPlayers() @@ -574,16 +565,37 @@ void boss_flame_leviathan::ScheduleEvents() void boss_flame_leviathan::SpellHit(Unit* /*caster*/, SpellInfo const* spellInfo) { - if (spellInfo->Id == SPELL_SYSTEMS_SHUTDOWN) + if (spellInfo->Id == SPELL_OVERLOAD_CIRCUIT) + { + ++_overloadCircuitCount; + if (_overloadCircuitCount == 1) + Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD_START); + + uint8 const threshold = me->GetMap()->Is25ManRaid() ? 4 : 2; + if (_overloadCircuitCount >= threshold) + { + _overloadCircuitCount = 0; + me->CastSpell(me, SPELL_SYSTEMS_SHUTDOWN, true); + } + } + else if (spellInfo->Id == SPELL_SYSTEMS_SHUTDOWN) { _shutdown = true; // ACHIEVEMENT - Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD); - Talk(FLAME_LEVIATHAN_EMOTE_REPAIR); Talk(FLAME_LEVIATHAN_SAY_OVERLOAD); + Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD); - events.DelayEvents(21ms); - events.ScheduleEvent(EVENT_REINSTALL, 20ms); + events.ScheduleEvent(EVENT_EJECT_PLAYERS, 3s); + } + else if (spellInfo->Id == SPELL_START_THE_ENGINE) + { + // Respawn turrets + for (uint8 i = 0; i < 4; ++i) + if (Unit* seat = vehicle->GetPassenger(i)) + if (seat->IsCreature()) + seat->ToCreature()->AI()->EnterEvadeMode(); + + Talk(FLAME_LEVIATHAN_EMOTE_REACTIVATE); } else if (spellInfo->Id == 62522 /*SPELL_ELECTROSHOCK*/) me->InterruptNonMeleeSpells(false); @@ -667,7 +679,11 @@ struct boss_flame_leviathan_seat : public VehicleAI } Vehicle* vehicle; - uint32 _despawnTimer; + + // Despawn 2 seats in 10-man. + static constexpr uint32 DESPAWN_DELAY_10MAN = 2000; + bool _pending10ManDespawn; + uint32 _despawnCheckTimer; void EnterEvadeMode(EvadeReason /*why*/) override { @@ -676,19 +692,20 @@ struct boss_flame_leviathan_seat : public VehicleAI void Reset() override { - _despawnTimer = !me->GetMap()->Is25ManRaid(); + _pending10ManDespawn = !me->GetMap()->Is25ManRaid(); + _despawnCheckTimer = 0; } void UpdateAI(uint32 diff) override { - if (_despawnTimer) + if (_pending10ManDespawn) { - _despawnTimer += diff; - if (_despawnTimer >= 2000) + _despawnCheckTimer += diff; + if (_despawnCheckTimer >= DESPAWN_DELAY_10MAN) { - _despawnTimer = 0; - if (Vehicle* veh = me->GetVehicle()) - if (veh->GetPassenger(0) == me || veh->GetPassenger(1) == me) + _pending10ManDespawn = false; + if (Vehicle* parentVehicle = me->GetVehicle()) + if (parentVehicle->GetPassenger(0) == me || parentVehicle->GetPassenger(1) == me) me->DespawnOrUnsummon(1ms); } } @@ -711,6 +728,14 @@ struct boss_flame_leviathan_seat : public VehicleAI { if (Unit* turret = me->GetVehicleKit()->GetPassenger(SEAT_TURRET)) { + if (apply) + { + who->RemoveAurasDueToSpell(SPELL_HOOKSHOT); + who->RemoveAurasDueToSpell(SPELL_HOOKSHOT_AURA); + } + else + who->CastSpell(who, SPELL_SMOKE_TRAIL, true); + if (apply) { turret->ReplaceAllUnitFlags(UNIT_FLAG_NONE); @@ -726,6 +751,14 @@ struct boss_flame_leviathan_seat : public VehicleAI turret->ToCreature()->AI()->EnterEvadeMode(); } } + if (Unit* device = me->GetVehicleKit()->GetPassenger(SEAT_DEVICE)) + { + if (apply) + device->SetNpcFlag(UNIT_NPC_FLAG_SPELLCLICK); + else + device->RemoveNpcFlag(UNIT_NPC_FLAG_SPELLCLICK); + } + } } }; @@ -755,12 +788,9 @@ struct boss_flame_leviathan_defense_turret : public TurretAI if (Player* player = killer->ToPlayer()) player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS, 1, 0, me); - if (Vehicle* vehicle = me->GetVehicle()) - if (Unit* device = vehicle->GetPassenger(SEAT_DEVICE)) - device->ReplaceAllUnitFlags(UNIT_FLAG_NONE); // unselectable - - if (Creature* leviathan = _instance->GetCreature(BOSS_LEVIATHAN)) - leviathan->AI()->DoAction(ACTION_DESTROYED_TURRET); + if (Vehicle* seatVehicle = me->GetVehicle()) + if (Unit* device = seatVehicle->GetPassenger(SEAT_DEVICE)) + device->CastSpell(device, SPELL_OVERLOAD_CIRCUIT, true); } bool CanAIAttack(Unit const* who) const override @@ -806,10 +836,7 @@ struct boss_flame_leviathan_overload_device : public NullCreatureAI me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); if (Unit* player = me->GetVehicle()->GetPassenger(SEAT_PLAYER)) - { - me->GetVehicleBase()->CastSpell(player, SPELL_SMOKE_TRAIL, true); player->ExitVehicle(); - } } } }; @@ -1245,7 +1272,7 @@ class spell_systems_shutdown_aura : public AuraScript bool Validate(SpellInfo const* /*spellInfo*/) override { - return ValidateSpellInfo({ SPELL_GATHERING_SPEED }); + return ValidateSpellInfo({ SPELL_GATHERING_SPEED, SPELL_OVERLOAD_CIRCUIT, SPELL_START_THE_ENGINE }); } void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) @@ -1256,6 +1283,7 @@ class spell_systems_shutdown_aura : public AuraScript owner->SetControlled(true, UNIT_STATE_STUNNED); owner->RemoveAurasDueToSpell(SPELL_GATHERING_SPEED); + owner->RemoveAurasDueToSpell(SPELL_OVERLOAD_CIRCUIT); if (Vehicle* vehicle = owner->GetVehicleKit()) if (Unit* cannon = vehicle->GetPassenger(SEAT_CANNON)) cannon->GetAI()->DoAction(ACTION_DELAY_CANNON); @@ -1268,6 +1296,7 @@ class spell_systems_shutdown_aura : public AuraScript return; owner->SetControlled(false, UNIT_STATE_STUNNED); + owner->CastSpell(owner, SPELL_START_THE_ENGINE, true); } void Register() override @@ -1352,50 +1381,58 @@ class spell_vehicle_throw_passenger : public SpellScript { PrepareSpellScript(spell_vehicle_throw_passenger); + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_HOOKSHOT_AURA }); + } + void HandleScript() { - Spell* baseSpell = GetSpell(); - SpellCastTargets targets = baseSpell->m_targets; - if (Vehicle* vehicle = GetCaster()->GetVehicleKit()) - if (Unit* passenger = vehicle->GetPassenger(3)) + Vehicle* vehicle = GetCaster()->GetVehicleKit(); + Unit* passenger = vehicle ? vehicle->GetPassenger(3) : nullptr; + if (!vehicle || !passenger) + return; + + // Find nearest unoccupied Flame Leviathan seat near trajectory destination + Position const* dst = GetExplTargetDest(); + if (!dst) + return; + + constexpr float SEARCH_RADIUS = 99.0f; + + std::list targetList; + Acore::WorldObjectSpellAreaTargetCheck check(SEARCH_RADIUS, dst, GetCaster(), GetCaster(), GetSpellInfo(), TARGET_CHECK_DEFAULT, nullptr); + Acore::WorldObjectListSearcher searcher(GetCaster(), targetList, check); + Cell::VisitObjects(GetCaster(), searcher, SEARCH_RADIUS); + + Unit* seatTarget = nullptr; + float minDist = SEARCH_RADIUS * SEARCH_RADIUS; + + for (WorldObject* obj : targetList) + { + Unit* unit = obj->ToUnit(); + if (!unit || unit->GetEntry() != NPC_SEAT) continue; + + Vehicle* seat = unit->GetVehicleKit(); + Unit* device = seat ? seat->GetPassenger(SEAT_DEVICE) : nullptr; + if (!seat || seat->GetPassenger(0) || !device || device->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) continue; + + float dist = unit->GetExactDistSq(dst); + if (dist < minDist) { - // use 99 because it is 3d search - std::list targetList; - Acore::WorldObjectSpellAreaTargetCheck check(99, GetExplTargetDest(), GetCaster(), GetCaster(), GetSpellInfo(), TARGET_CHECK_DEFAULT, nullptr); - Acore::WorldObjectListSearcher searcher(GetCaster(), targetList, check); - Cell::VisitObjects(GetCaster(), searcher, 99.0f); - float minDist = 99 * 99; - Unit* target = nullptr; - for (std::list::iterator itr = targetList.begin(); itr != targetList.end(); ++itr) - { - if (Unit* unit = (*itr)->ToUnit()) - if (unit->GetEntry() == NPC_SEAT) - if (Vehicle* seat = unit->GetVehicleKit()) - if (!seat->GetPassenger(0)) - if (Unit* device = seat->GetPassenger(2)) - if (!device->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) - { - float dist = unit->GetExactDistSq(targets.GetDstPos()); - if (dist < minDist) - { - minDist = dist; - target = unit; - } - } - } - if (target && target->IsWithinDist2d(targets.GetDstPos(), GetSpellInfo()->Effects[EFFECT_0].CalcRadius() * 2)) // now we use *2 because the location of the seat is not correct - { - passenger->ExitVehicle(); - passenger->EnterVehicle(target, 0); - } - else - { - passenger->ExitVehicle(); - float x, y, z; - targets.GetDstPos()->GetPosition(x, y, z); - passenger->GetMotionMaster()->MoveJump(x, y, z, targets.GetSpeedXY(), targets.GetSpeedZ()); - } + minDist = dist; + seatTarget = unit; } + } + + // Launch passenger toward destination + float x, y, z; + dst->GetPosition(x, y, z); + passenger->ExitVehicle(); + passenger->GetMotionMaster()->MoveJump(x, y, z, GetSpell()->m_targets.GetSpeedXY(), GetSpell()->m_targets.GetSpeedZ()); + + if (seatTarget && seatTarget->IsWithinDist2d(dst, GetSpellInfo()->Effects[EFFECT_0].CalcRadius() * 2)) // now we use *2 because the location of the seat is not correct + passenger->CastCustomSpell(SPELL_HOOKSHOT_AURA, SPELLVALUE_AURA_DURATION, 5000, passenger, true); } void Register() override @@ -1404,6 +1441,28 @@ class spell_vehicle_throw_passenger : public SpellScript } }; +// 62336 Hookshot Aura +class spell_hookshot_aura : public AuraScript +{ + PrepareAuraScript(spell_hookshot_aura); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_HOOKSHOT }); + } + + void OnPeriodic(AuraEffect const* aurEff) + { + PreventDefaultAction(); + GetUnitOwner()->CastSpell(GetUnitOwner(), GetSpellInfo()->Effects[aurEff->GetEffIndex()].TriggerSpell, true); + } + + void Register() override + { + OnEffectPeriodic += AuraEffectPeriodicFn(spell_hookshot_aura::OnPeriodic, EFFECT_0, SPELL_AURA_PERIODIC_DUMMY); + } +}; + class spell_tar_blaze_aura : public AuraScript { PrepareAuraScript(spell_tar_blaze_aura); @@ -1476,31 +1535,6 @@ class spell_vehicle_grab_pyrite : public SpellScript } }; -class spell_vehicle_circuit_overload_aura : public AuraScript -{ - PrepareAuraScript(spell_vehicle_circuit_overload_aura); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_SYSTEMS_SHUTDOWN }); - } - - void OnPeriodic(AuraEffect const* /*aurEff*/) - { - if (Unit* target = GetTarget()) - if (int(target->GetAppliedAuras().count(SPELL_OVERLOAD_CIRCUIT)) >= (target->GetMap()->Is25ManRaid() ? 4 : 2)) - { - target->CastSpell(target, SPELL_SYSTEMS_SHUTDOWN, true); - target->RemoveAurasDueToSpell(SPELL_OVERLOAD_CIRCUIT); - } - } - - void Register() override - { - OnEffectPeriodic += AuraEffectPeriodicFn(spell_vehicle_circuit_overload_aura::OnPeriodic, EFFECT_1, SPELL_AURA_PERIODIC_DUMMY); - } -}; - class spell_orbital_supports_aura : public AuraScript { PrepareAuraScript(spell_orbital_supports_aura); @@ -1757,9 +1791,9 @@ void AddSC_boss_flame_leviathan() RegisterSpellScript(spell_systems_shutdown_aura); RegisterSpellScript(spell_pursue); RegisterSpellScript(spell_vehicle_throw_passenger); + RegisterSpellScript(spell_hookshot_aura); RegisterSpellScript(spell_tar_blaze_aura); RegisterSpellScript(spell_vehicle_grab_pyrite); - RegisterSpellScript(spell_vehicle_circuit_overload_aura); RegisterSpellScript(spell_orbital_supports_aura); RegisterSpellScript(spell_thorims_hammer); RegisterSpellScript(spell_transitus_shield_beam_aura); From c09afb0803f09b8348e8e442d4ca84d007b2bb7e Mon Sep 17 00:00:00 2001 From: sogladev Date: Sat, 4 Jul 2026 22:44:54 +0200 Subject: [PATCH 14/47] fix(Scripts/Ulduar): rework XT-002 Deconstructor (#26345) Co-authored-by: Keader --- .../rev_1750785600_xt002_tc_rework.sql | 109 ++ .../Northrend/Ulduar/Ulduar/boss_xt002.cpp | 1553 +++++++++-------- .../Ulduar/Ulduar/instance_ulduar.cpp | 1 + .../scripts/Northrend/Ulduar/Ulduar/ulduar.h | 4 + 4 files changed, 926 insertions(+), 741 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql diff --git a/data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql b/data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql new file mode 100644 index 000000000..ffdfb1164 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql @@ -0,0 +1,109 @@ +-- +-- XT-002 Deconstructor: rework + +-- Vehicle accessory: auto-spawn Heart of the Deconstructor when XT-002 spawns +DELETE FROM `vehicle_template_accessory` WHERE `entry` = 33293 AND `accessory_entry` = 33329; +INSERT INTO `vehicle_template_accessory` (`entry`, `accessory_entry`, `seat_id`, `minion`, `description`, `summontype`, `summontimer`) VALUES +(33293, 33329, 0, 0, 'XT-002 Deconstructor - Heart', 6, 30000); + +-- Spell script names for exposed heart and energy orb +DELETE FROM `spell_script_names` WHERE `ScriptName` IN ('spell_xt002_exposed_heart', 'spell_xt002_energy_orb'); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(63849, 'spell_xt002_exposed_heart'), +(62826, 'spell_xt002_energy_orb'); + +-- Spell script name for Heart Overload periodic trigger (server-side spell 62791) +DELETE FROM `spell_script_names` WHERE `spell_id`=62791 AND `ScriptName`='spell_xt002_heart_overload_periodic'; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(62791, 'spell_xt002_heart_overload_periodic'); + +-- Update Gravity Bomb ScriptNames +UPDATE `spell_script_names` SET `ScriptName`='spell_xt002_gravity_bomb_aura' WHERE `spell_id` IN (63024, 64234) AND `ScriptName`='spell_xt002_gravity_bomb'; + +-- Spell script name for generic Submerge spell +DELETE FROM `spell_script_names` WHERE `ScriptName`='spell_xt002_submerged' AND `spell_id`=37751; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(37751, 'spell_xt002_submerged'); + +-- Spell script name for generic Stand spell +DELETE FROM `spell_script_names` WHERE `ScriptName`='spell_xt002_stand' AND `spell_id`=37752; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(37752, 'spell_xt002_stand'); + +-- Disable spell proc on Exposed Heart effect_1 (handled by script) +DELETE FROM `spell_proc` WHERE `SpellId`=63849; +INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES +(63849, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0); + +-- Replace waypoint_data for XT-002 pre-combat patrol path +DELETE FROM `waypoint_data` WHERE `id`=1360540; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `delay`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1360540, 1, 862.5053, 7.207682, 409.8612, 0, 0, 0, 0, 100, 0), +(1360540, 2, 863.4635, 25.65489, 409.8612, 0, 0, 0, 0, 100, 0), +(1360540, 3, 872.6903, 39.44819, 409.8355, 0, 11000, 0, 0, 100, 0), -- MovementInform: emote 468 +(1360540, 4, 869.8901, 32.82189, 409.8509, 0, 0, 0, 0, 100, 0), +(1360540, 5, 854.8365, 9.631022, 409.8612, 0, 0, 0, 0, 100, 0), +(1360540, 6, 855.48, -19.90913, 409.8787, 0, 0, 0, 0, 100, 0), +(1360540, 7, 852.1145, -44.87088, 409.8869, 0, 0, 0, 0, 100, 0), +(1360540, 8, 864.5853, -62.56917, 409.6369, 0, 0, 0, 0, 100, 0), +(1360540, 9, 876.4196, -77.23026, 409.9155, 0, 11000, 0, 0, 100, 0), -- MovementInform: emote 468 +(1360540, 10, 878.3914, -79.4183, 409.9155, 0, 0, 0, 0, 100, 0), +(1360540, 11, 870.9197, -65.75331, 409.9155, 0, 0, 0, 0, 100, 0), +(1360540, 12, 880.2524, -33.8488, 409.9155, 0, 0, 0, 0, 100, 0), +(1360540, 13, 883.6281, -12.63596, 409.799, 3.159046, 30000, 0, 0, 100, 0); -- MovementInform: emote 10 + +DELETE FROM `waypoint_scripts` WHERE `guid`=5 and `id` = 1360540; +DELETE FROM `waypoint_scripts` WHERE `guid`=6 and `id` = 1360541; + +-- Update script names for adds +UPDATE `creature_template` SET `ScriptName`='npc_scrapbot' WHERE `entry`=33343; +UPDATE `creature_template` SET `ScriptName`='npc_pummeller' WHERE `entry`=33344; +UPDATE `creature_template` SET `ScriptName`='npc_boombot' WHERE `entry`=33346; +UPDATE `creature_template` SET `ScriptName`='npc_life_spark' WHERE `entry`=34004; +UPDATE `creature_template` SET `ScriptName`='npc_xt_void_zone', `AIName` = '' WHERE `entry`=34001; + +-- Base attack time for XT-002 (both 10 and 25) from 1800 +UPDATE `creature_template` SET `BaseAttackTime`=2000 WHERE `entry` IN (33293, 33885); + +-- Achievement script names +UPDATE `achievement_criteria_data` SET `ScriptName`='achievement_nerf_engineering' WHERE `ScriptName`='achievement_xt002_nerf_engineering'; +UPDATE `achievement_criteria_data` SET `ScriptName`='achievement_nerf_gravity_bombs' WHERE `ScriptName`='achievement_xt002_nerf_gravity_bombs'; + +-- Achievement heartbreaker: type=11 script entries for hard mode (criteria 10072=25M, 10073=10M) +DELETE FROM `achievement_criteria_data` WHERE `criteria_id` IN (10072,10073) AND `type`=11; +INSERT INTO `achievement_criteria_data` (`criteria_id`, `type`, `value1`, `value2`, `ScriptName`) VALUES +(10072, 11, 0, 0, 'achievement_heartbreaker'), +(10073, 11, 0, 0, 'achievement_heartbreaker'); + +-- Mechanical immunities for Heart of the Deconstructor and XT-002 (both 10 and 25) +SET @ID := -427; + +DELETE FROM `creature_immunities` WHERE `ID` = @ID; +INSERT INTO `creature_immunities` (`ID`, `SchoolMask`, `DispelTypeMask`, `MechanicsMask`, `Effects`, `Auras`, `ImmuneAoE`, `ImmuneChain`, `Comment`) VALUES +(@ID, 0, 0, 617299839, '98,124,144,145', 0, 0, 0, 'mech=0x26CB3F7F(CHARM|DISORIENTED|DISARM|DISTRACT|FEAR|GRIP|ROOT|SILENCE|SLEEP|SNARE|STUN|FREEZE|KNOCKOUT|POLYMORPH|BANISH|SHACKLE|TURN|HORROR|DAZE|SAPPED), flags=IMMUNITY_KNOCKBACK, effects=98(KNOCK_BACK),124(PULL_TOWARDS),144(KNOCK_BACK_DEST),145(PULL_TOWARDS_DEST)'); + +UPDATE `creature_template` SET `CreatureImmunitiesId` = @ID WHERE (`entry` IN (33329, 33995, 33293, 33885)); + +-- Cannot turn +UPDATE `creature_template` SET `unit_flags2` = `unit_flags2` | 32768 WHERE (`entry` IN (33329, 33995)); + +-- only hits XT-002 +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 64799) AND (`SourceId` = 0) AND (`ElseGroup` IN (0, 1)) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` IN (33293, 33885)) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 64799, 0, 0, 31, 0, 3, 33293, 0, 0, 0, 0, '', 'target must be \'XT-002 Deconstructor\''), +(13, 1, 64799, 0, 1, 31, 0, 3, 33885, 0, 0, 0, 0, '', 'target must be \'XT-002 Deconstructor (1)\''); + +UPDATE `spell_proc` SET `AttributesMask`=0, `DisableEffectsMask`=2 WHERE `SpellId`=63849; + +-- Toy Piles: spawn at 4 corners of the room (positions from TC, map 603 Ulduar) +SET @CGUID := 12777; +SET @BUILD := 0; +DELETE FROM `creature` WHERE (`id` = 33337) AND (`guid` BETWEEN @CGUID+0 AND @CGUID+3); +INSERT INTO `creature` (`guid`, `id`, `map`, `zoneId`, `areaId`, `spawnMask`, `phaseMask`, `equipment_id`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecs`, `wander_distance`, `currentwaypoint`, `curhealth`, `curmana`, `MovementType`, `npcflag`, `unit_flags`, `dynamicflags`, `ScriptName`, `Comment`, `VerifiedBuild`) VALUES +(@CGUID+0, 33337, 603, 0, 0, 3, 1, 0, 897.908, 67.0764, 412.129, 3.92699, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD), +(@CGUID+1, 33337, 603, 0, 0, 3, 1, 0, 898.099, -88.9115, 409.887, 2.23402, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD), +(@CGUID+2, 33337, 603, 0, 0, 3, 1, 0, 793.096, -95.158, 409.887, 0.855211, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD), +(@CGUID+3, 33337, 603, 0, 0, 3, 1, 0, 792.646, 65.3854, 414.147, 5.20108, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD); + +DELETE FROM `spell_script_names` WHERE `spell_id`=62826 AND `ScriptName`='spell_xt002_energy_orb'; +UPDATE `creature_template` SET `AIName` = '', `ScriptName` = 'npc_xt_toy_pile' WHERE (`entry` = 33337); diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp index d6260a114..75820d461 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp @@ -16,259 +16,337 @@ */ #include "AchievementCriteriaScript.h" +#include "Containers.h" #include "CreatureScript.h" +#include "InstanceScript.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" #include "Opcodes.h" #include "PassiveAI.h" #include "Player.h" #include "ScriptedCreature.h" #include "SpellAuraEffects.h" +#include "SpellMgr.h" #include "SpellScript.h" #include "SpellScriptLoader.h" -#include "Vehicle.h" #include "ulduar.h" +#include "Vehicle.h" +#include "WorldPacket.h" -enum XT002Spells +enum Spells { - // BASIC - SPELL_GRAVITY_BOMB = 63024, - SPELL_SEARING_LIGHT = 63018, - SPELL_TYMPANIC_TANTARUM = 62776, - SPELL_XT002_ENRAGE = 26662, + SPELL_TYMPANIC_TANTRUM = 62776, + SPELL_SEARING_LIGHT = 63018, + SPELL_SUMMON_LIFE_SPARK = 64210, + SPELL_SUMMON_VOID_ZONE = 64203, + SPELL_GRAVITY_BOMB = 63024, + SPELL_HEARTBREAK = 65737, + SPELL_STAND = 37752, + SPELL_SUBMERGE = 37751, + SPELL_ENRAGE = 26662, + SPELL_COOLDOWN_CREATURE_SPECIAL_2 = 64404, + SPELL_SCRAP_REPAIR = 62832, - // HELPERS - SPELL_ARCING_SMASH = 8374, - SPELL_TRAMPLE = 5568, - SPELL_UPPERCUT = 10966, - SPELL_BOOM = 62834, + // XT-Toy Pile + SPELL_RECHARGE_PUMMELER = 62831, + SPELL_RECHARGE_SCRAPBOT = 62828, + SPELL_RECHARGE_BOOMBOT = 62835, - // HEARTBREAK - SPELL_HEART_OVERLOAD = 62789, - SPELL_EXPOSED_HEART = 63849, - SPELL_ENERGY_ORB = 62790, - SPELL_ENERGY_ORB_TRIGGER = 62826, - SPELL_HEARTBREAK = 65737, + // Heart of the Deconstructor + SPELL_ENERGY_ORB = 62790, + SPELL_ENERGY_ORB_TRIGGERED = 62826, + SPELL_RIDE_VEHICLE_EXPOSED = 63313, + SPELL_EXPOSED_HEART = 63849, + SPELL_HEART_RIDE_VEHICLE = 63852, + SPELL_SCRAPBOT_RIDE_VEHICLE = 47020, + SPELL_FULL_HEAL = 17683, + SPELL_HEART_OVERLOAD = 62789, + SPELL_HEART_OVERLOAD_TRIGGER = 62791, + SPELL_HEART_LIGHTNING_TETHER = 64799, - // VOID ZONE - SPELL_VOID_ZONE_SUMMON = 64203, - SPELL_VOID_ZONE_DAMAGE = 64208, + // Void Zone + SPELL_CONSUMPTION = 64209, - // SPARK - SPELL_SPARK_SUMMON = 64210, - SPELL_SPARK_DAMAGE = 64227, - SPELL_SPARK_MELEE = 64230, + // Life Spark + SPELL_ARCANE_POWER_STATE = 49411, + SPELL_STATIC_CHARGED = 64227, + SPELL_SHOCK = 64230, - // ACHIEVEMENT - SPELL_ACHIEVEMENT_CREDIT_NERF_SCRAPBOTS = 65037, + // XM-024 Pummeller + SPELL_ARCING_SMASH = 8374, + SPELL_TRAMPLE = 5568, + SPELL_UPPERCUT = 10966, + + //Boombot + SPELL_321_BOOMBOT_AURA = 65032, + SPELL_BOOM = 62834, + + // Achievement-related spells + SPELL_ACHIEVEMENT_CREDIT_NERF_SCRAPBOTS = 65037 }; -enum XT002Events +enum Events { - EVENT_HEALTH_CHECK = 1, - EVENT_GRAVITY_BOMB = 2, - EVENT_SEARING_LIGHT = 3, - EVENT_ENRAGE = 4, - EVENT_TYMPANIC_TANTARUM = 5, - EVENT_RESTORE = 6, - EVENT_START_SECOND_PHASE = 7, - EVENT_REMOVE_EMOTE = 8, - EVENT_CHECK_ROOM = 9, + EVENT_TYMPANIC_TANTRUM = 1, + EVENT_PHASE_CHECK, + EVENT_SEARING_LIGHT, + EVENT_GRAVITY_BOMB, + EVENT_SUBMERGE, + EVENT_DISPOSE_HEART, + EVENT_ENRAGE, + EVENT_ENTER_HARD_MODE, + EVENT_RESUME_ATTACK }; -enum NPCs +enum XT002Phases { - NPC_VOID_ZONE = 34001, - NPC_LIFE_SPARK = 34004, - NPC_XT002_HEART = 33329, - NPC_XS013_SCRAPBOT = 33343, - NPC_XM024_PUMMELLER = 33344, - NPC_XE321_BOOMBOT = 33346, - NPC_PILE_TRIGGER = 33337, + PHASE_1 = 1, + PHASE_HEART }; -enum Texts +enum Actions { - SAY_AGGRO = 0, - SAY_HEART_OPENED = 1, - SAY_HEART_CLOSED = 2, - SAY_TYMPANIC_TANTRUM = 3, - SAY_SLAY = 4, - SAY_BERSERK = 5, - SAY_DEATH = 6, - SAY_SUMMON = 7, - EMOTE_HEART_OPENED = 8, - EMOTE_HEART_CLOSED = 9, - EMOTE_TYMPANIC_TANTRUM = 10, - EMOTE_SCRAPBOT = 11, + ACTION_ENTER_HARD_MODE, + ACTION_START_PHASE_HEART, + ACTION_DISPOSE_HEART +}; + +enum XT002Data +{ + DATA_TRANSFERED_HEALTH, + DATA_HARD_MODE, + DATA_HEALTH_RECOVERED, + DATA_GRAVITY_BOMB_CASUALTY +}; + +enum Yells +{ + SAY_AGGRO = 0, + SAY_HEART_OPENED = 1, + SAY_HEART_CLOSED = 2, + SAY_TYMPANIC_TANTRUM = 3, + SAY_SLAY = 4, + SAY_BERSERK = 5, + SAY_DEATH = 6, + SAY_SUMMON = 7, + EMOTE_HEART_OPENED = 8, + EMOTE_HEART_CLOSED = 9, + EMOTE_TYMPANIC_TANTRUM = 10, + EMOTE_SCRAPBOT = 11 }; enum Misc { - HEART_VEHICLE_SEAT = 0, - - ACTION_AWAKEN_HEART = -5, - ACTION_HIDE_HEART = -4, - ACTION_HEART_BROKEN = -3, - - ACHIEVEMENT_MUST_DECONSTRUCT_FASTER = 21027, - - DATA_XT002_NERF_ENGINEERING = 50, - DATA_XT002_GRAVITY_ACHIEV = 51, + ACHIEV_MUST_DECONSTRUCT_FASTER = 21027, + HEART_VEHICLE_SEAT_EXPOSED = 1, + GROUP_SEARING_GRAVITY = 1 }; struct boss_xt002 : public BossAI { - boss_xt002(Creature* pCreature) : BossAI(pCreature, BOSS_XT002) { } - - uint8 _healthCheck; - bool _hardMode; - bool _nerfAchievement; - bool _gravityAchievement; - - void RescheduleEvents() + boss_xt002(Creature* creature) : BossAI(creature, BOSS_XT002) { - events.RescheduleEvent(EVENT_GRAVITY_BOMB, 1s, 1); - events.RescheduleEvent(EVENT_TYMPANIC_TANTARUM, 1min, 1); - if (!_hardMode) - events.RescheduleEvent(EVENT_HEALTH_CHECK, 2s, 1); + Initialize(); + } + + void Initialize() + { + _healthRecovered = false; + _gravityBombCasualty = false; + _hardMode = false; + _exposeHeartPercent = 75; + } + + void ChangeNextExpose() + { + switch (_exposeHeartPercent) + { + case 75: + _exposeHeartPercent = 50; + break; + case 50: + _exposeHeartPercent = 25; + break; + default: + _exposeHeartPercent = 0; + break; + } } void Reset() override { _Reset(); - - me->ResetLootMode(); - me->RemoveAllAuras(); - - // first heart expose - _healthCheck = 75; - _hardMode = false; - _nerfAchievement = true; - _gravityAchievement = true; - - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(false, UNIT_STATE_STUNNED); - - if (instance) - { - instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEVEMENT_MUST_DECONSTRUCT_FASTER); - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_ACTIVE); - } + DoCastSelf(SPELL_STAND); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + events.SetPhase(PHASE_1); + me->SetReactState(REACT_AGGRESSIVE); + me->SetNoCallForHelp(true); // Skip pulling nearby + Initialize(); + instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_MUST_DECONSTRUCT_FASTER); } - void AttachHeart() + void EnterEvadeMode(EvadeReason /*why*/) override { - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->SetHealth(heart->GetMaxHealth()); - else if (Creature* accessory = me->SummonCreature(NPC_XT002_HEART, *me, TEMPSUMMON_MANUAL_DESPAWN)) - { - accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); - if (!me->HandleSpellClick(accessory, 0)) - accessory->DespawnOrUnsummon(); - } + summons.DespawnAll(); + BossAI::EnterEvadeMode(); } - void JustReachedHome() override + void JustEngagedWith(Unit* who) override { - _JustReachedHome(); - me->setActive(false); - } - - void JustEngagedWith(Unit*) override - { - me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); - events.ScheduleEvent(EVENT_ENRAGE, 10min, 0, 0); - events.ScheduleEvent(EVENT_CHECK_ROOM, 5s, 0, 0); - RescheduleEvents(); // Other events are scheduled here - - me->setActive(true); Talk(SAY_AGGRO); + BossAI::JustEngagedWith(who); + events.ScheduleEvent(EVENT_SEARING_LIGHT, Is25ManRaid() ? 9s : 11s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_GRAVITY_BOMB, Is25ManRaid() ? 18s : 21s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_ENRAGE, 10min); + events.ScheduleEvent(EVENT_TYMPANIC_TANTRUM, 60s, 0, PHASE_1); + events.ScheduleEvent(EVENT_PHASE_CHECK, 1s, 0, PHASE_1); + instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_MUST_DECONSTRUCT_FASTER); - if (instance) - { - instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEVEMENT_MUST_DECONSTRUCT_FASTER); - instance->SetBossState(BOSS_XT002, IN_PROGRESS); - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_READY); - } - - me->CallForHelp(175); - me->SetInCombatWithZone(); - AttachHeart(); + // AC core difference: Vehicle::InstallAccessory despawns the heart if HandleSpellClick + // returns false (TC uses async VehicleJoinEvent and does not check return value). + // Ensure the heart exists as a fallback. + if (!instance->GetCreature(DATA_XT002_HEART)) + if (Creature* heart = me->SummonCreature(NPC_HEART_OF_DECONSTRUCTOR, *me, TEMPSUMMON_MANUAL_DESPAWN)) + me->HandleSpellClick(heart, 0); } - void KilledUnit(Unit* victim) override + void DoAction(int32 action) override { - if (victim->IsPlayer() && !urand(0, 2)) - { + if (action == ACTION_ENTER_HARD_MODE) + events.ScheduleEvent(EVENT_ENTER_HARD_MODE, 1ms); + } + + void KilledUnit(Unit* who) override + { + if (who->IsPlayer()) Talk(SAY_SLAY); - } } void JustDied(Unit* /*killer*/) override { Talk(SAY_DEATH); _JustDied(); - - if (instance) - { - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_ACTIVE); - } + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); } - void DoAction(int32 param) override + void ExposeHeart() { - if (param == DATA_XT002_NERF_ENGINEERING) + events.SetPhase(PHASE_HEART); + me->SetReactState(REACT_PASSIVE); + me->AttackStop(); + Talk(SAY_HEART_OPENED); + events.CancelEvent(EVENT_TYMPANIC_TANTRUM); + events.ScheduleEvent(EVENT_SUBMERGE, 6s, 0, PHASE_HEART); + ChangeNextExpose(); + } + + void DisposeHeart(bool isHardMode = false) + { + Talk(SAY_HEART_CLOSED); + + if (isHardMode) { - _nerfAchievement = false; - return; + me->SetReactState(REACT_AGGRESSIVE); + RescheduleEvents(); } - if (param == DATA_XT002_GRAVITY_ACHIEV) + else { - _gravityAchievement = false; - return; - } - - if (!me->IsAlive() || _hardMode) - return; - - // heart destory - if (param == ACTION_HEART_BROKEN) - { - _hardMode = true; - me->SetLootMode(3); // hard mode + normal loot - me->SetMaxHealth(me->GetMaxHealth()); - me->SetHealth(me->GetMaxHealth()); - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - - me->CastSpell(me, SPELL_HEARTBREAK, true); - Talk(EMOTE_HEART_CLOSED); - events.ScheduleEvent(EVENT_REMOVE_EMOTE, 4s); - return; + events.ScheduleEvent(EVENT_RESUME_ATTACK, 1s, 0, PHASE_HEART); } - // damage from heart - if (param > 0) + DoCastSelf(SPELL_STAND); + DoCastSelf(SPELL_COOLDOWN_CREATURE_SPECIAL_2); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + if (Creature* heart = instance->GetCreature(DATA_XT002_HEART)) { - // avoid reducing health under 1 - int32 _final = std::min(param, int32(me->GetHealth() - 1)); + if (heart->IsAlive()) + heart->AI()->DoAction(ACTION_DISPOSE_HEART); + else + heart->DespawnOrUnsummon(); + } - me->ModifyHealth(-_final); - me->LowerPlayerDamageReq(_final); + } + + void RescheduleEvents() + { + events.SetPhase(PHASE_1); + events.ScheduleEvent(EVENT_SEARING_LIGHT, 25s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_GRAVITY_BOMB, Is25ManRaid() ? 33s : 15s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_TYMPANIC_TANTRUM, 25s, 0, PHASE_1); + if (!_hardMode) + events.ScheduleEvent(EVENT_PHASE_CHECK, 1s, 0, PHASE_1); + } + + void PassengerBoarded(Unit* who, int8 seatId, bool apply) override + { + if (!apply) + return; + + if (who->GetEntry() == NPC_XS013_SCRAPBOT) + { + Talk(EMOTE_SCRAPBOT); + _healthRecovered = true; + } + else if (seatId == HEART_VEHICLE_SEAT_EXPOSED) + who->CastSpell(who, SPELL_EXPOSED_HEART); // Channeled + } + + void MovementInform(uint32 type, uint32 point) override + { + if (type != WAYPOINT_MOTION_TYPE) + return; + + switch (point) + { + case 3: + case 9: + me->HandleEmoteCommand(EMOTE_STATE_SPELL_CHANNEL_OMNI); + break; + case 13: + me->HandleEmoteCommand(EMOTE_STATE_WORK); + break; + default: + break; } } - uint32 GetData(uint32 param) const override + uint32 GetData(uint32 type) const override { - if (param == DATA_XT002_NERF_ENGINEERING) - return _nerfAchievement; - else if (param == DATA_XT002_GRAVITY_ACHIEV) - return _gravityAchievement; + switch (type) + { + case DATA_HARD_MODE: + return _hardMode ? 1 : 0; + case DATA_HEALTH_RECOVERED: + return _healthRecovered ? 1 : 0; + case DATA_GRAVITY_BOMB_CASUALTY: + return _gravityBombCasualty ? 1 : 0; + default: + return 0; + } + } - return 0; + void SetData(uint32 type, uint32 data) override + { + switch (type) + { + case DATA_TRANSFERED_HEALTH: + if (!_hardMode) + { + uint32 transferHealth = data; + if (transferHealth >= me->GetHealth()) + transferHealth = me->GetHealth() - 1; + + me->ModifyHealth(-static_cast(transferHealth)); + me->LowerPlayerDamageReq(transferHealth); + } + break; + case DATA_GRAVITY_BOMB_CASUALTY: + _gravityBombCasualty = (data > 0) ? true : false; + break; + default: + break; + } } void UpdateAI(uint32 diff) override @@ -277,553 +355,368 @@ struct boss_xt002 : public BossAI return; events.Update(diff); + if (me->HasUnitState(UNIT_STATE_CASTING)) return; - switch (events.ExecuteEvent()) + while (uint32 eventId = events.ExecuteEvent()) { - // Control events - case EVENT_HEALTH_CHECK: - if (_hardMode) - { - return; - } + switch (eventId) + { + case EVENT_SEARING_LIGHT: + DoCastSelf(SPELL_SEARING_LIGHT); + events.Repeat(Is25ManRaid() ? 16s : 20s); + break; + case EVENT_GRAVITY_BOMB: + DoCastSelf(SPELL_GRAVITY_BOMB); + events.Repeat(Is25ManRaid() ? 16s : 20s); + break; + case EVENT_TYMPANIC_TANTRUM: + Talk(SAY_TYMPANIC_TANTRUM); + Talk(EMOTE_TYMPANIC_TANTRUM); + events.DelayEvents(10s, GROUP_SEARING_GRAVITY); + DoCastSelf(SPELL_TYMPANIC_TANTRUM); + events.Repeat(1min); + break; + case EVENT_PHASE_CHECK: + if (me->HealthBelowPct(_exposeHeartPercent)) + ExposeHeart(); + events.Repeat(1s); + break; + case EVENT_SUBMERGE: + DoCastSelf(SPELL_SUBMERGE); + me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + Talk(EMOTE_HEART_OPENED); + if (Creature* heart = instance->GetCreature(DATA_XT002_HEART)) + heart->AI()->DoAction(ACTION_START_PHASE_HEART); + events.ScheduleEvent(EVENT_DISPOSE_HEART, 30s, PHASE_HEART); + break; + case EVENT_DISPOSE_HEART: + DisposeHeart(); + break; + case EVENT_ENRAGE: + Talk(SAY_BERSERK); + DoCastSelf(SPELL_ENRAGE); + break; + case EVENT_ENTER_HARD_MODE: + me->SetFullHealth(); + DoCastSelf(SPELL_HEARTBREAK, true); + me->AddLootMode(LOOT_MODE_HARD_MODE_1); + _hardMode = true; + DisposeHeart(_hardMode); + break; + case EVENT_RESUME_ATTACK: + me->SetReactState(REACT_AGGRESSIVE); + RescheduleEvents(); + break; + default: + break; + } - if (me->HealthBelowPct(_healthCheck)) - { - _healthCheck -= 25; - me->SetControlled(true, UNIT_STATE_STUNNED); - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_SUBMERGED); // submerge with animation - - Talk(SAY_HEART_OPENED); - - events.CancelEventGroup(1); - events.ScheduleEvent(EVENT_START_SECOND_PHASE, 5s); - return; - } - events.Repeat(1s); - break; - case EVENT_CHECK_ROOM: - events.Repeat(5s); - if (me->GetPositionX() < 722 || me->GetPositionX() > 987 || me->GetPositionY() < -139 || me->GetPositionY() > 124) - EnterEvadeMode(); - - return; - - // Abilities events - case EVENT_GRAVITY_BOMB: - me->CastCustomSpell(SPELL_GRAVITY_BOMB, SPELLVALUE_MAX_TARGETS, 1, me, true); - events.ScheduleEvent(EVENT_SEARING_LIGHT, 10s, 1); - break; - case EVENT_SEARING_LIGHT: - me->CastCustomSpell(SPELL_SEARING_LIGHT, SPELLVALUE_MAX_TARGETS, 1, me, true); - events.ScheduleEvent(EVENT_GRAVITY_BOMB, 10s, 1); - break; - case EVENT_TYMPANIC_TANTARUM: - Talk(EMOTE_TYMPANIC_TANTRUM); - Talk(SAY_TYMPANIC_TANTRUM); - me->CastSpell(me, SPELL_TYMPANIC_TANTARUM, true); - events.Repeat(1min); - return; - case EVENT_ENRAGE: - Talk(SAY_BERSERK); - me->CastSpell(me, SPELL_XT002_ENRAGE, true); - break; - - // Animation events - case EVENT_START_SECOND_PHASE: - Talk(EMOTE_HEART_OPENED); - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->GetAI()->DoAction(ACTION_AWAKEN_HEART); - - events.ScheduleEvent(EVENT_RESTORE, 30s); - return; - // Restore from heartbreak - case EVENT_RESTORE: - if (_hardMode) - { - return; - } - - Talk(SAY_HEART_CLOSED); - - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - // Hide heart - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->GetAI()->DoAction(ACTION_HIDE_HEART); - - events.ScheduleEvent(EVENT_REMOVE_EMOTE, 4s); - return; - case EVENT_REMOVE_EMOTE: - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(false, UNIT_STATE_STUNNED); - - RescheduleEvents(); + if (me->HasUnitState(UNIT_STATE_CASTING)) return; } - // Disabled by stunned state - DoMeleeAttackIfReady(); - } -}; - -struct npc_xt002_heart : public PassiveAI -{ - npc_xt002_heart(Creature* pCreature) : PassiveAI(pCreature), summons(me) - { - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - } - - SummonList summons; - uint32 _damageDone; - uint32 _timerSpawn; - - uint8 _spawnSelection; - uint8 _pummelerCount; - - void MoveInLineOfSight(Unit*) override { } - void AttackStart(Unit*) override { } - void JustSummoned(Creature* cr) override - { - summons.Summon(cr); - if (Unit* owner = me->GetVehicleBase()) - if (owner->IsCreature()) - owner->ToCreature()->AI()->JustSummoned(cr); - } - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - _damageDone += damage; - } - - void SummonPiles() - { - me->SummonCreature(NPC_PILE_TRIGGER, 893.290f, 66.820f, 409.81f, 4.2f); - me->SummonCreature(NPC_PILE_TRIGGER, 898.099f, -88.9115f, 409.887f, 2.23402f); - me->SummonCreature(NPC_PILE_TRIGGER, 793.096f, -95.158f, 409.887f, 0.855211f); - me->SummonCreature(NPC_PILE_TRIGGER, 794.600f, 59.660f, 409.82f, 5.34f); - } - - void DoAction(int32 param) override - { - if (param == ACTION_AWAKEN_HEART) - { - _pummelerCount = 0; - _spawnSelection = 0; - _damageDone = 0; - _timerSpawn = 0; - me->SetHealth(me->GetMaxHealth()); - me->CastSpell(me, SPELL_HEART_OVERLOAD, true); - me->CastSpell(me, SPELL_EXPOSED_HEART, false); // Channeled - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - - if (!summons.HasEntry(NPC_PILE_TRIGGER)) - SummonPiles(); - } - else if (param == ACTION_HIDE_HEART) - { - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - if (pXT002->AI()) - { - pXT002->AI()->DoAction(_damageDone); - _damageDone = 0; - } - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - } - } - - void SendEnergyToCorner() - { - Unit* pile = nullptr; - uint8 num = urand(1, 4); - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - if (summon->GetEntry() == NPC_PILE_TRIGGER) - { - pile = summon; - if ((--num) == 0) - break; - } - - if (pile) - me->CastSpell(pile, SPELL_ENERGY_ORB, true); - } - - void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override - { - // spawn not-so-random robots - if (spellInfo->Id == SPELL_ENERGY_ORB_TRIGGER && target->GetEntry() == NPC_PILE_TRIGGER) - switch (_spawnSelection) - { - case 0: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection++; - break; - case 1: - me->SummonCreature(NPC_XE321_BOOMBOT, target->GetPositionX(), target->GetPositionY(), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 5000); - _spawnSelection++; - break; - case 2: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection++; - break; - case 3: - if (_pummelerCount < 2) - me->SummonCreature(NPC_XM024_PUMMELLER, target->GetPositionX(), target->GetPositionY(), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 5000); - - _pummelerCount++; - _spawnSelection++; - break; - case 4: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection = 0; - break; - } - } - - void JustDied(Unit* /*killer*/) override - { - me->SetVisible(false); - if (me->GetInstanceScript()) - if (Creature* XT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - if (XT002->AI()) - XT002->AI()->DoAction(ACTION_HEART_BROKEN); - } - - void UpdateAI(uint32 diff) override - { - if (!me->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE)) - { - _timerSpawn += diff; - if (_timerSpawn >= 1900) - { - SendEnergyToCorner(); - _timerSpawn -= 1900; - } - } - } -}; - -struct npc_xt002_scrapbot : public PassiveAI -{ - npc_xt002_scrapbot(Creature* pCreature) : PassiveAI(pCreature) { } - - bool _locked; - void Reset() override - { - me->StopMoving(); - _locked = true; - me->SetWalk(true); - - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->GetPositionZ() > 411.0f) // he is on stairs... idiot cryness protection - me->GetMotionMaster()->MovePoint(0, 884.028931f, -14.593809f, 409.786987f); - else - _locked = false; - } - } - - void JustDied(Unit* killer) override - { - // Nerf Scrapbots achievement - if (killer && killer->GetEntry() == NPC_XE321_BOOMBOT) - if (me->GetInstanceScript()) - { - me->GetInstanceScript()->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_SPELL_TARGET, 65037); - me->GetInstanceScript()->DoUpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET, 65037); - } - } - - // tc use updateAI, while we have movementinform - void MovementInform(uint32 type, uint32 /*param*/) override - { - if (type == POINT_MOTION_TYPE) - { - _locked = false; - return; - } - - // we reached the target :) - if (type == FOLLOW_MOTION_TYPE && me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->IsAlive()) - { - pXT002->AI()->DoAction(DATA_XT002_NERF_ENGINEERING); - pXT002->ModifyHealth(pXT002->GetMaxHealth() * 0.01f); - } - - if (!urand(0, 2)) - pXT002->AI()->Talk(EMOTE_SCRAPBOT); - - me->DespawnOrUnsummon(1ms); - } - } - - void UpdateAI(uint32 /*diff*/) override - { - if (!_locked) - { - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - me->GetMotionMaster()->MoveFollow(pXT002, 0.0f, 0.0f); - _locked = true; - } - } - } -}; - -struct npc_xt002_pummeller : public ScriptedAI -{ - npc_xt002_pummeller(Creature* pCreature) : ScriptedAI(pCreature) { } - - int32 _arcingSmashTimer; - int32 _trampleTimer; - int32 _uppercutTimer; - - void Reset() override - { - _arcingSmashTimer = 0; - _trampleTimer = 0; - _uppercutTimer = 0; - - if (Unit* target = SelectTargetFromPlayerList(200)) - AttackStart(target); - else - me->DespawnOrUnsummon(500ms); - } - - void UpdateAI(uint32 diff) override - { - if (!UpdateVictim()) - return; - - _arcingSmashTimer += diff; - _trampleTimer += diff; - _uppercutTimer += diff; - - if (_arcingSmashTimer >= 8000) - { - me->CastSpell(me->GetVictim(), SPELL_ARCING_SMASH, false); - _arcingSmashTimer = 0; - return; - } - if (_trampleTimer >= 11000) - { - me->CastSpell(me->GetVictim(), SPELL_TRAMPLE, false); - _trampleTimer = 0; - return; - } - if (_uppercutTimer >= 14000) - { - me->CastSpell(me->GetVictim(), SPELL_UPPERCUT, false); - _uppercutTimer = 0; - return; - } - - DoMeleeAttackIfReady(); - } -}; - -class BoomEvent : public BasicEvent -{ -public: - BoomEvent(Creature* me) : _me(me) - { - } - - bool Execute(uint64 /*time*/, uint32 /*diff*/) override - { - // This hack is here because we suspect our implementation of spell effect execution on targets - // is done in the wrong order. We suspect that EFFECT_0 needs to be applied on all targets, - // then EFFECT_1, etc - instead of applying each effect on target1, then target2, etc. - // The above situation causes the visual for this spell to be bugged, so we remove the instakill - // effect and implement a script hack for that. - - _me->CastSpell(_me, SPELL_BOOM, false); - return true; + if (events.IsInPhase(PHASE_1)) + DoMeleeAttackIfReady(); } private: - Creature* _me; + bool _healthRecovered; // Did a scrapbot recover XT-002's health during the encounter? + bool _hardMode; // Are we in hard mode? Or: was the heart killed during phase 2? + bool _gravityBombCasualty; // Did someone die because of Gravity Bomb damage? + uint8 _exposeHeartPercent; }; -struct npc_xt002_boombot : public PassiveAI +struct npc_xt002_heart : public NullCreatureAI { - npc_xt002_boombot(Creature* pCreature) : PassiveAI(pCreature) { } + explicit npc_xt002_heart(Creature* creature) : NullCreatureAI(creature), _instance(creature->GetInstanceScript()) { } - bool _locked; - bool _boomed; void Reset() override { - me->StopMoving(); - _locked = true; - _boomed = false; - me->SetUnitMovementFlags(MOVEMENTFLAG_WALKING); - - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->GetPositionZ() > 411.0f) // he is on stairs... idiot cryness protection - me->GetMotionMaster()->MovePoint(0, 884.028931f, -14.593809f, 409.786987f); - else - _locked = false; - } + me->SetRegeneratingHealth(false); } - void Explode() + void DoAction(int32 action) override { - if (_boomed) + Creature* xt002 = _instance->GetCreature(BOSS_XT002); + if (!xt002) return; - _boomed = true; // Prevent recursive calls - - WorldPacket data(SMSG_SPELLINSTAKILLLOG, 8 + 8 + 4); - data << me->GetGUID(); - data << me->GetGUID(); - data << uint32(SPELL_BOOM); - me->SendMessageToSet(&data, false); - - me->KillSelf(); - - // Visual only seems to work if the instant kill event is delayed or the spell itself is delayed - // Casting done from player and caster source has the same targetinfo flags, - // so that can't be the issue - // See BoomEvent class - // Schedule 1s delayed - me->m_Events.AddEventAtOffset(new BoomEvent(me), 1s); + if (action == ACTION_START_PHASE_HEART) + { + DoCastSelf(SPELL_FULL_HEAL); + DoCast(xt002, SPELL_RIDE_VEHICLE_EXPOSED, true); + DoCastSelf(SPELL_HEART_OVERLOAD, true); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->SetUnitFlag(UNIT_FLAG_PREVENT_EMOTES_FROM_CHAT_TEXT); + } + else if (action == ACTION_DISPOSE_HEART) + { + DoCast(xt002, SPELL_HEART_RIDE_VEHICLE, true); + me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->RemoveUnitFlag(UNIT_FLAG_PREVENT_EMOTES_FROM_CHAT_TEXT); + } } void JustDied(Unit* /*killer*/) override { - me->m_Events.AddEventAtOffset(new BoomEvent(me), 1s); + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->DoAction(ACTION_ENTER_HARD_MODE); } - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - if (_boomed) - damage = 0; - - if (me->HealthBelowPctDamaged(50, damage) && !_boomed) - { - damage = 0; - Explode(); - } - } - - // tc they use updateAI, while we have movementinform - void MovementInform(uint32 type, uint32 /*param*/) override - { - if (type == POINT_MOTION_TYPE) - { - _locked = false; - return; - } - // we reached the target :) - //if (type == FOLLOW_MOTION_TYPE) - // _kill = true; - } - - void UpdateAI(uint32 /*diff*/) override - { - if (!_locked) - { - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - me->GetMotionMaster()->MoveFollow(pXT002, 0.0f, 0.0f); - _locked = true; - } - } - } +private: + InstanceScript * _instance; }; -struct npc_xt002_life_spark : public ScriptedAI +struct npc_scrapbot : public ScriptedAI { - npc_xt002_life_spark(Creature* pCreature) : ScriptedAI(pCreature) - { - me->SetMaxHealth(RAID_MODE(54000, 172000)); - me->SetHealth(me->GetMaxHealth()); - me->CastSpell(me, SPELL_SPARK_DAMAGE, true); - } + npc_scrapbot(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()) { } - uint32 _attackTimer; void Reset() override { - if (Unit* target = SelectTargetFromPlayerList(200)) - AttackStart(target); - else + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { me->DespawnOrUnsummon(); + return; + } + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(2s, [this](TaskContext /*StartMove*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + me->GetMotionMaster()->MoveFollow(xt002, 0.0f, 0.0f); + }) + .Schedule(1s, [this](TaskContext checkXt002) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + { + if (me->IsWithinMeleeRange(xt002)) + { + DoCast(xt002, SPELL_SCRAPBOT_RIDE_VEHICLE); + _scheduler.Schedule(1s, [this](TaskContext /*ScrapRepair*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->CastSpell(me, SPELL_SCRAP_REPAIR, true); + me->DespawnOrUnsummon(1s); + }); + } + else + checkXt002.Repeat(); + } + else + me->DespawnOrUnsummon(); + }); } - void UpdateAI(uint32 /*diff*/) override + void UpdateAI(uint32 diff) override + { + _scheduler.Update(diff); + } + +private: + InstanceScript* _instance; + TaskScheduler _scheduler; +}; + +struct npc_pummeller : public ScriptedAI +{ + npc_pummeller(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()) { } + + void Reset() override + { + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { + me->DespawnOrUnsummon(); + return; + } + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(1s, [this](TaskContext /*StartMove*/) + { + me->SetReactState(REACT_AGGRESSIVE); + DoZoneInCombat(); + }) + .Schedule(17s, [this](TaskContext trample) + { + DoCastSelf(SPELL_TRAMPLE); + trample.Repeat(11s); + }) + .Schedule(19s, [this](TaskContext arcingSmash) + { + DoCastSelf(SPELL_ARCING_SMASH); + arcingSmash.Repeat(8s); + }) + .Schedule(19s, [this](TaskContext upperCut) + { + DoCastVictim(SPELL_UPPERCUT); + upperCut.Repeat(14s); + }); + + } + + void UpdateAI(uint32 diff) override { if (!UpdateVictim()) return; - me->CastSpell(me->GetVictim(), SPELL_SPARK_MELEE, false); - DoMeleeAttackIfReady(); + _scheduler.Update(diff, [this] + { + DoMeleeAttackIfReady(); + }); } + +private: + InstanceScript* _instance; + TaskScheduler _scheduler; }; -// 62775 - Tympanic Tantrum -class spell_xt002_tympanic_tantrum : public SpellScript +struct npc_boombot : public ScriptedAI { - PrepareSpellScript(spell_xt002_tympanic_tantrum); + npc_boombot(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()), _boomed(false) { } - void FilterTargets(std::list& targets) + void Reset() override { - targets.remove_if(PlayerOrPetCheck()); + DoCastSelf(SPELL_321_BOOMBOT_AURA); + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { + me->DespawnOrUnsummon(); + return; + } + + // HACK/workaround: + // these values aren't confirmed - lack of data - and the values in DB are incorrect + // these values are needed for correct damage of Boom spell + me->SetFloatValue(UNIT_FIELD_MINDAMAGE, 15000.0f); + me->SetFloatValue(UNIT_FIELD_MAXDAMAGE, 18000.0f); + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(4s, [this](TaskContext /*StartMove*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + me->GetMotionMaster()->MoveFollow(xt002, 0.0f, 0.0f); + + }) + .Schedule(1s, [this](TaskContext checkXt002) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + { + if (me->IsWithinMeleeRange(xt002)) + DoCastAOE(SPELL_BOOM); + else + checkXt002.Repeat(); + } + else + me->DespawnOrUnsummon(); + }); } - void RecalculateDamage() + void DamageTaken(Unit* /*attacker*/, uint32& damage, DamageEffectType /*dmgType*/, SpellSchoolMask /*school*/) override { - if (GetHitUnit()) - SetHitDamage(GetHitUnit()->CountPctFromMaxHealth(GetHitDamage())); + if (damage >= (me->GetHealth() - me->GetMaxHealth() * 0.5f) && !_boomed) + { + _boomed = true; // Prevent recursive call + damage = 0; + DoCastAOE(SPELL_BOOM); + } } - void Register() override + void UpdateAI(uint32 diff) override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_ALL, TARGET_UNIT_SRC_AREA_ENEMY); - OnHit += SpellHitFn(spell_xt002_tympanic_tantrum::RecalculateDamage); + _scheduler.Update(diff); } + +private: + InstanceScript* _instance; + bool _boomed; + TaskScheduler _scheduler; }; -// 64234, 63024 - Gravity Bomb -enum GravityBomb +struct npc_life_spark : public ScriptedAI { - SPELL_GRAVITY_BOMB_TRIGGER_10 = 63025 -}; + npc_life_spark(Creature* creature) : ScriptedAI(creature) { } -class spell_xt002_gravity_bomb : public SpellScript -{ - PrepareSpellScript(spell_xt002_gravity_bomb); - - void SelectTarget(std::list& targets) + void Reset() override { - if (Unit* victim = GetCaster()->GetVictim()) - targets.remove_if(Acore::ObjectGUIDCheck(victim->GetGUID(), true)); + DoCastSelf(SPELL_ARCANE_POWER_STATE); + _scheduler.CancelAll(); } - void Register() override + void JustEngagedWith(Unit* /*who*/) override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_gravity_bomb::SelectTarget, EFFECT_ALL, TARGET_UNIT_DEST_AREA_ENEMY); + DoCastSelf(SPELL_STATIC_CHARGED); + _scheduler.Schedule(12s, [this](TaskContext spellShock) + { + DoCastVictim(SPELL_SHOCK); + spellShock.Repeat(); + }); } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim()) + return; + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + _scheduler.Update(diff, [this] + { + DoMeleeAttackIfReady(); + }); + } + +private: + TaskScheduler _scheduler; }; -class spell_xt002_gravity_bomb_aura : public AuraScript +struct npc_xt_void_zone : public PassiveAI { - PrepareAuraScript(spell_xt002_gravity_bomb_aura); + npc_xt_void_zone(Creature* creature) : PassiveAI(creature) { } - bool Validate(SpellInfo const* /*spellInfo*/) override + void Reset() override { - return ValidateSpellInfo({ SPELL_VOID_ZONE_DAMAGE }); + _scheduler.Schedule(2500ms, [this](TaskContext /*task*/) + { + DoCastSelf(SPELL_CONSUMPTION); + }); + } + + void UpdateAI(uint32 diff) override + { + _scheduler.Update(diff); + } + +private: + TaskScheduler _scheduler; +}; + +// 63018, 65121 - Searing Light +class spell_xt002_searing_light_spawn_life_spark : public AuraScript +{ + PrepareAuraScript(spell_xt002_searing_light_spawn_life_spark); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_SUMMON_LIFE_SPARK }); } void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) @@ -831,26 +724,42 @@ class spell_xt002_gravity_bomb_aura : public AuraScript if (Player* player = GetOwner()->ToPlayer()) if (Unit* xt002 = GetCaster()) if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode - if (Creature* creature = xt002->SummonCreature(NPC_VOID_ZONE, player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000)) - { - int32 damage = GetSpellInfo()->Id == SPELL_GRAVITY_BOMB_TRIGGER_10 ? 5000 : 7500; - creature->CastCustomSpell(creature, SPELL_VOID_ZONE_DAMAGE, &damage, 0, 0, true); - } + xt002->CastSpell(player, SPELL_SUMMON_LIFE_SPARK, true); + } + + void Register() override + { + AfterEffectRemove += AuraEffectRemoveFn(spell_xt002_searing_light_spawn_life_spark::OnRemove, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL, AURA_EFFECT_HANDLE_REAL); + } +}; + +// 63024, 64234 - Gravity Bomb +class spell_xt002_gravity_bomb_aura : public AuraScript +{ + PrepareAuraScript(spell_xt002_gravity_bomb_aura); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_SUMMON_VOID_ZONE }); + } + + void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) + { + if (Player* player = GetOwner()->ToPlayer()) + if (Unit* xt002 = GetCaster()) + if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode + xt002->CastSpell(player, SPELL_SUMMON_VOID_ZONE, true); } void OnPeriodic(AuraEffect const* aurEff) { Unit* xt002 = GetCaster(); + Unit* owner = GetTarget(); if (!xt002) return; - Unit* owner = GetOwner()->ToUnit(); - if (!owner) - return; - if (aurEff->GetAmount() >= int32(owner->GetHealth())) - if (xt002->GetAI()) - xt002->GetAI()->DoAction(DATA_XT002_GRAVITY_ACHIEV); + xt002->GetAI()->SetData(DATA_GRAVITY_BOMB_CASUALTY, 1); } void Register() override @@ -860,20 +769,17 @@ class spell_xt002_gravity_bomb_aura : public AuraScript } }; -// 64233, 63025 - Gravity Bomb +// 63025, 64233 - Gravity Bomb (Damage) class spell_xt002_gravity_bomb_damage : public SpellScript { PrepareSpellScript(spell_xt002_gravity_bomb_damage); - void HandleScript(SpellEffIndex /*eff*/) + void HandleScript(SpellEffIndex /*effIndex*/) { - Unit* caster = GetCaster(); - if (!caster) - return; - if (GetHitDamage() >= int32(GetHitUnit()->GetHealth())) - if (caster->GetAI()) - caster->GetAI()->DoAction(DATA_XT002_GRAVITY_ACHIEV); + if (InstanceScript* instance = GetCaster()->GetInstanceScript()) + if (Creature* xt002 = instance->GetCreature(BOSS_XT002)) + xt002->AI()->SetData(DATA_GRAVITY_BOMB_CASUALTY, 1); } void Register() override @@ -882,74 +788,119 @@ class spell_xt002_gravity_bomb_damage : public SpellScript } }; -// 63018, 65121 - Searing Light -class spell_xt002_searing_light_spawn_life_spark : public SpellScript +// 62791 - XT-002 Heart Overload Trigger Spell (server-side) +// Fires an Energy Orb (62790) at a random XT-Toy Pile. +// The distance check for whether to summon adds happens in spell_xt002_energy_orb (62826). +class spell_xt002_heart_overload_periodic : public SpellScript { - PrepareSpellScript(spell_xt002_searing_light_spawn_life_spark); + PrepareSpellScript(spell_xt002_heart_overload_periodic); - void SelectTarget(std::list& targets) + bool Validate(SpellInfo const* /*spell*/) override { - if (Unit* victim = GetCaster()->GetVictim()) - targets.remove_if(Acore::ObjectGUIDCheck(victim->GetGUID(), true)); + return ValidateSpellInfo({SPELL_ENERGY_ORB, SPELL_HEART_LIGHTNING_TETHER}); + } + + static constexpr float ToyPileSearchDistance = 250.0f; + + static Creature* GetRandomToyPile(Unit const* caster) + { + std::list targets; + caster->GetCreatureListWithEntryInGrid(targets, NPC_XT_TOY_PILE, ToyPileSearchDistance); + + if (targets.empty()) + return nullptr; + + return Acore::Containers::SelectRandomContainerElement(targets); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + Unit* caster = GetCaster(); + caster->CastSpell((Unit*)nullptr, SPELL_HEART_LIGHTNING_TETHER, true); + if (Creature* toyPile = GetRandomToyPile(caster)) + caster->CastSpell(toyPile, SPELL_ENERGY_ORB, true); } void Register() override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_searing_light_spawn_life_spark::SelectTarget, EFFECT_ALL, TARGET_UNIT_DEST_AREA_ENEMY); + OnEffectHit += SpellEffectFn(spell_xt002_heart_overload_periodic::HandleScript, EFFECT_0, SPELL_EFFECT_DUMMY); } }; -class spell_xt002_searing_light_spawn_life_spark_aura : public AuraScript +struct npc_xt_toy_pile : public ScriptedAI { - PrepareAuraScript(spell_xt002_searing_light_spawn_life_spark_aura); - - void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) + explicit npc_xt_toy_pile(Creature* creature) : ScriptedAI(creature) { - if (Player* player = GetOwner()->ToPlayer()) - if (Unit* xt002 = GetCaster()) - if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode - xt002->SummonCreature(NPC_LIFE_SPARK, player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000); + _lastSummonTime = 0; + } + + static constexpr float SummonDistance = 90.0f; // large enough so XT can be tanked at the door + static constexpr uint32 SummonCooldown = 12 * IN_MILLISECONDS; // verified with sniff + + void SpellHit(Unit* caster, SpellInfo const* spell) override + { + if (spell->Id != SPELL_ENERGY_ORB_TRIGGERED) + return; + + Creature* xt002 = caster ? caster->GetVehicleCreatureBase() : nullptr; + if (!xt002 || xt002->IsWithinDist(me, SummonDistance)) + return; + + uint32 now = getMSTime(); + if (now - _lastSummonTime < SummonCooldown) + return; + + _lastSummonTime = now; + + DoCastSelf(SPELL_RECHARGE_BOOMBOT, true); + + if (roll_chance_i(30)) + DoCastSelf(SPELL_RECHARGE_PUMMELER, true); + + uint8 const summonCount = urand(5, 7); + for (uint8 i = 0; i < summonCount; ++i) + DoCastSelf(SPELL_RECHARGE_SCRAPBOT, true); + + xt002->AI()->Talk(SAY_SUMMON); + } + +private: + uint32 _lastSummonTime{}; +}; + +// 62775 - Tympanic Tantrum +class spell_xt002_tympanic_tantrum : public SpellScript +{ + PrepareSpellScript(spell_xt002_tympanic_tantrum); + + void FilterTargets(std::list& targets) + { + targets.remove_if([](WorldObject* object) -> bool + { + if (object->IsPlayer()) + return false; + + if (Creature* creature = object->ToCreature()) + return !creature->IsPet(); + + return true; + }); + } + + void RecalculateDamage() + { + SetHitDamage(GetHitUnit()->CountPctFromMaxHealth(GetHitDamage())); } void Register() override { - OnEffectRemove += AuraEffectRemoveFn(spell_xt002_searing_light_spawn_life_spark_aura::OnRemove, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL, AURA_EFFECT_HANDLE_REAL); + OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_0, TARGET_UNIT_SRC_AREA_ENEMY); + OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_1, TARGET_UNIT_SRC_AREA_ENEMY); + OnHit += SpellHitFn(spell_xt002_tympanic_tantrum::RecalculateDamage); } }; -class achievement_xt002_nerf_engineering : public AchievementCriteriaScript -{ -public: - achievement_xt002_nerf_engineering() : AchievementCriteriaScript("achievement_xt002_nerf_engineering") {} - - bool OnCheck(Player* /*player*/, Unit* target, uint32 /*criteria_id*/) override - { - if (target) - if (InstanceScript* instance = target->GetInstanceScript()) - if (Creature* cr = instance->GetCreature(BOSS_XT002)) - return cr->AI()->GetData(DATA_XT002_NERF_ENGINEERING); - - return false; - } -}; - -class achievement_xt002_nerf_gravity_bombs : public AchievementCriteriaScript -{ -public: - achievement_xt002_nerf_gravity_bombs() : AchievementCriteriaScript("achievement_xt002_nerf_gravity_bombs") {} - - bool OnCheck(Player* /*player*/, Unit* target, uint32 /*criteria_id*/) override - { - if (target) - if (InstanceScript* instance = target->GetInstanceScript()) - if (Creature* cr = instance->GetCreature(BOSS_XT002)) - return cr->AI()->GetData(DATA_XT002_GRAVITY_ACHIEV); - - return false; - } -}; - -// 65032 - 321 Boombot Aura +// 65032 - 321-Boombot Aura class spell_xt002_321_boombot_aura : public AuraScript { PrepareAuraScript(spell_xt002_321_boombot_aura); @@ -979,24 +930,144 @@ class spell_xt002_321_boombot_aura : public AuraScript } }; +// 63849 - Exposed Heart +// Transfers damage dealt to the Heart to XT-002 on every hit. +// Also fires Energy Orb missiles at Toy Piles on damage taken, with a 1s cooldown. +class spell_xt002_exposed_heart : public AuraScript +{ + PrepareAuraScript(spell_xt002_exposed_heart); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_HEART_OVERLOAD_TRIGGER }); + } + + static constexpr uint32 OrbCooldown = 1500; + + void OnProc(AuraEffect const* /*aurEff*/, ProcEventInfo& eventInfo) + { + PreventDefaultAction(); + DamageInfo* damageInfo = eventInfo.GetDamageInfo(); + if (!damageInfo || !damageInfo->GetDamage()) + return; + + if (Creature* xt002 = GetTarget()->GetVehicleCreatureBase()) + xt002->AI()->SetData(DATA_TRANSFERED_HEALTH, damageInfo->GetDamage()); + + uint32 now = getMSTime(); + if (now - _lastOrbTime >= OrbCooldown) + { + _lastOrbTime = now; + if (Unit* caster = GetCaster()) + caster->CastSpell(caster, SPELL_HEART_OVERLOAD_TRIGGER, true); + } + } + + void Register() override + { + OnEffectProc += AuraEffectProcFn(spell_xt002_exposed_heart::OnProc, EFFECT_0, SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN); + } + +private: + uint32 _lastOrbTime{}; +}; + +// 37751 - Submerged +class spell_xt002_submerged : public SpellScript +{ + PrepareSpellScript(spell_xt002_submerged); + + void HandleScript(SpellEffIndex /*eff*/) + { + if (Creature* target = GetHitCreature()) + target->SetStandState(UNIT_STAND_STATE_SUBMERGED); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_xt002_submerged::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 37752 - Stand +class spell_xt002_stand : public SpellScript +{ + PrepareSpellScript(spell_xt002_stand); + + void HandleScript(SpellEffIndex /*eff*/) + { + if (Creature* target = GetHitCreature()) + target->SetStandState(UNIT_STAND_STATE_STAND); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_xt002_stand::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +class achievement_nerf_engineering : public AchievementCriteriaScript +{ + public: + achievement_nerf_engineering() : AchievementCriteriaScript("achievement_nerf_engineering") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return !(target->GetAI()->GetData(DATA_HEALTH_RECOVERED)); + } +}; + +class achievement_heartbreaker : public AchievementCriteriaScript +{ + public: + achievement_heartbreaker() : AchievementCriteriaScript("achievement_heartbreaker") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return target->GetAI()->GetData(DATA_HARD_MODE) != 0; + } +}; + +class achievement_nerf_gravity_bombs : public AchievementCriteriaScript +{ + public: + achievement_nerf_gravity_bombs() : AchievementCriteriaScript("achievement_nerf_gravity_bombs") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return !(target->GetAI()->GetData(DATA_GRAVITY_BOMB_CASUALTY)); + } +}; + void AddSC_boss_xt002() { - // Npcs RegisterUlduarCreatureAI(boss_xt002); RegisterUlduarCreatureAI(npc_xt002_heart); - RegisterUlduarCreatureAI(npc_xt002_scrapbot); - RegisterUlduarCreatureAI(npc_xt002_pummeller); - RegisterUlduarCreatureAI(npc_xt002_boombot); - RegisterUlduarCreatureAI(npc_xt002_life_spark); - - // Spells - RegisterSpellScript(spell_xt002_tympanic_tantrum); - RegisterSpellAndAuraScriptPair(spell_xt002_gravity_bomb, spell_xt002_gravity_bomb_aura); + RegisterUlduarCreatureAI(npc_scrapbot); + RegisterUlduarCreatureAI(npc_pummeller); + RegisterUlduarCreatureAI(npc_boombot); + RegisterUlduarCreatureAI(npc_life_spark); + RegisterUlduarCreatureAI(npc_xt_void_zone); + RegisterSpellScript(spell_xt002_searing_light_spawn_life_spark); + RegisterSpellScript(spell_xt002_gravity_bomb_aura); RegisterSpellScript(spell_xt002_gravity_bomb_damage); - RegisterSpellAndAuraScriptPair(spell_xt002_searing_light_spawn_life_spark, spell_xt002_searing_light_spawn_life_spark_aura); + RegisterSpellScript(spell_xt002_heart_overload_periodic); + RegisterUlduarCreatureAI(npc_xt_toy_pile); + RegisterSpellScript(spell_xt002_tympanic_tantrum); RegisterSpellScript(spell_xt002_321_boombot_aura); - - // Achievements - new achievement_xt002_nerf_engineering(); - new achievement_xt002_nerf_gravity_bombs(); + RegisterSpellScript(spell_xt002_exposed_heart); + RegisterSpellScript(spell_xt002_submerged); + RegisterSpellScript(spell_xt002_stand); + new achievement_nerf_engineering(); + new achievement_heartbreaker(); + new achievement_nerf_gravity_bombs(); } diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 5712fe72f..b54ff8b97 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -88,6 +88,7 @@ ObjectData const creatureData[] = { NPC_IGNIS, BOSS_IGNIS }, { NPC_RAZORSCALE, BOSS_RAZORSCALE }, { NPC_XT002, BOSS_XT002 }, + { NPC_HEART_OF_DECONSTRUCTOR,DATA_XT002_HEART }, { NPC_KOLOGARN, BOSS_KOLOGARN }, { NPC_AURIAYA, BOSS_AURIAYA }, { NPC_MIMIRON, BOSS_MIMIRON }, diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h index eb50851c8..abf79d49d 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h @@ -87,6 +87,7 @@ enum UlduarData // XT-002 DATA_XT002_DOORS = 400, + DATA_XT002_HEART = 401, // Kologarn DATA_KOLOGARN_DOORS = 410, @@ -158,6 +159,9 @@ enum UlduarNPCs NPC_IGNIS = 33118, NPC_RAZORSCALE = 33186, NPC_XT002 = 33293, + NPC_XT_TOY_PILE = 33337, + NPC_XS013_SCRAPBOT = 33343, + NPC_HEART_OF_DECONSTRUCTOR = 33329, NPC_STEELBREAKER = 32867, NPC_MOLGEIM = 32927, NPC_BRUNDIR = 32857, From 583a38237ebd444f01cad227443b14c388887afb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 20:45:46 +0000 Subject: [PATCH 15/47] chore(DB): import pending files Referenced commit(s): 3776f464e5fc16bf363e2f28931b501648f1956f --- .../2026_07_04_03.sql} | 1 + .../rev_1782492883671113125.sql => db_world/2026_07_04_04.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1750785600_xt002_tc_rework.sql => db_world/2026_07_04_03.sql} (99%) rename data/sql/updates/{pending_db_world/rev_1782492883671113125.sql => db_world/2026_07_04_04.sql} (98%) diff --git a/data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql b/data/sql/updates/db_world/2026_07_04_03.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql rename to data/sql/updates/db_world/2026_07_04_03.sql index ffdfb1164..20c5be2a3 100644 --- a/data/sql/updates/pending_db_world/rev_1750785600_xt002_tc_rework.sql +++ b/data/sql/updates/db_world/2026_07_04_03.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_02 -> 2026_07_04_03 -- -- XT-002 Deconstructor: rework diff --git a/data/sql/updates/pending_db_world/rev_1782492883671113125.sql b/data/sql/updates/db_world/2026_07_04_04.sql similarity index 98% rename from data/sql/updates/pending_db_world/rev_1782492883671113125.sql rename to data/sql/updates/db_world/2026_07_04_04.sql index 0e3d3bc44..cc033eb52 100644 --- a/data/sql/updates/pending_db_world/rev_1782492883671113125.sql +++ b/data/sql/updates/db_world/2026_07_04_04.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_03 -> 2026_07_04_04 -- -- Flame Leviathan: Remove spell_linked_spell entries for SPELL_SYSTEMS_SHUTDOWN (62475). -- These effects are now handled explicitly via script From f38a08dbffad236996c84cb31b386d4d9fcf61bd Mon Sep 17 00:00:00 2001 From: Apatia <107246426+Apathyxia@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:53:04 +0200 Subject: [PATCH 16/47] fix(DB/item_template): Update TotemCategory for multiple totems (#26385) --- data/sql/updates/pending_db_world/totemfix.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/sql/updates/pending_db_world/totemfix.sql diff --git a/data/sql/updates/pending_db_world/totemfix.sql b/data/sql/updates/pending_db_world/totemfix.sql new file mode 100644 index 000000000..b2ee70e57 --- /dev/null +++ b/data/sql/updates/pending_db_world/totemfix.sql @@ -0,0 +1,2 @@ +-- Update TotemCategories for Savage and Hateful totems +UPDATE `item_template` SET `TotemCategory` = 21 WHERE (`entry` IN (42593, 42594, 42595, 42596, 42601, 42606)); From 3586fc95937979a8c2bb57eec5cb5ad2e506eb26 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 20:54:17 +0000 Subject: [PATCH 17/47] chore(DB): import pending files Referenced commit(s): f38a08dbffad236996c84cb31b386d4d9fcf61bd --- .../totemfix.sql => db_world/2026_07_04_05.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/totemfix.sql => db_world/2026_07_04_05.sql} (79%) diff --git a/data/sql/updates/pending_db_world/totemfix.sql b/data/sql/updates/db_world/2026_07_04_05.sql similarity index 79% rename from data/sql/updates/pending_db_world/totemfix.sql rename to data/sql/updates/db_world/2026_07_04_05.sql index b2ee70e57..688481cbf 100644 --- a/data/sql/updates/pending_db_world/totemfix.sql +++ b/data/sql/updates/db_world/2026_07_04_05.sql @@ -1,2 +1,3 @@ +-- DB update 2026_07_04_04 -> 2026_07_04_05 -- Update TotemCategories for Savage and Hateful totems UPDATE `item_template` SET `TotemCategory` = 21 WHERE (`entry` IN (42593, 42594, 42595, 42596, 42601, 42606)); From 8cca034053ddd32dd08d4021b356dee54f12d111 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:01:54 +0200 Subject: [PATCH 18/47] feat(Core/Commands): add .npc showloot to display corpse loot (#26374) --- .../rev_1782657620390367800.sql | 14 ++++++ src/server/game/Miscellaneous/Language.h | 11 ++++- src/server/scripts/Commands/cs_npc.cpp | 46 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 data/sql/updates/pending_db_world/rev_1782657620390367800.sql diff --git a/data/sql/updates/pending_db_world/rev_1782657620390367800.sql b/data/sql/updates/pending_db_world/rev_1782657620390367800.sql new file mode 100644 index 000000000..bd6c8f820 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1782657620390367800.sql @@ -0,0 +1,14 @@ +-- +-- Issue #18038: add .npc showloot command entry and matching acore_string rows. +DELETE FROM `command` WHERE `name` = 'npc showloot'; +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('npc showloot', 2, 'Syntax: .npc showloot\nShows the loot generated on the selected creature''s corpse.'); + +DELETE FROM `acore_string` WHERE `entry` IN (35456, 35457, 35458, 35459, 35460, 35461); +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35456, '{} is not dead, or its corpse contains no loot.', '{}이(가) 죽지 않았거나 시체에 전리품이 없습니다.', '{} n''est pas mort, ou son cadavre ne contient aucun butin.', '{} ist nicht tot, oder sein Leichnam enthält keine Beute.', '{} 未死亡,或其尸体上没有战利品。', '{} 未死亡,或其屍體上沒有戰利品。', '{} no está muerto, o su cadáver no contiene botín.', '{} no está muerto, o su cadáver no contiene botín.', '{} не убит или его труп не содержит добычи.'), +(35457, 'Loot for {} (Entry: {}):', '{} (ID: {})의 전리품:', 'Butin de {} (ID : {}) :', 'Beute von {} (ID: {}):', '{} (ID: {}) 的战利品:', '{} (ID: {}) 的戰利品:', 'Botín de {} (ID: {}):', 'Botín de {} (ID: {}):', 'Добыча с {} (ID: {}):'), +(35458, 'Money: {}g {}s {}c', '돈: {}금 {}은 {}동', 'Argent : {}po {}pa {}pc', 'Geld: {}G {}S {}K', '金币: {}金 {}银 {}铜', '金幣:{}金 {}銀 {}銅', 'Dinero: {}o {}p {}c', 'Dinero: {}o {}p {}c', 'Деньги: {}зол {}сер {}мед'), +(35459, 'Items ({}):', '아이템 ({}):', 'Objets ({}) :', 'Gegenstände ({}):', '物品 ({}):', '物品 ({}):', 'Objetos ({}):', 'Objetos ({}):', 'Предметы ({}):'), +(35460, ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (Entry: {})', ' {}개 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID : {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}个 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}個 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}шт. |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})'), +(35461, 'Quest items ({}):', '퀘스트 아이템 ({}):', 'Objets de quête ({}) :', 'Questgegenstände ({}):', '任务物品 ({}):', '任務物品 ({}):', 'Objetos de misión ({}):', 'Objetos de misión ({}):', 'Предметы заданий ({}):'); diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index a3cf7b59d..ef158770b 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1520,6 +1520,15 @@ enum AcoreStrings LANG_PET_RENAME_SUCCESS = 35454, // Wintergrasp shutdown deferral - LANG_WG_SHUTDOWN_DEFERRED = 35455 + LANG_WG_SHUTDOWN_DEFERRED = 35455, + + // npc showloot command + LANG_COMMAND_NOT_DEAD_OR_NO_LOOT = 35456, + LANG_COMMAND_NPC_SHOWLOOT_HEADER = 35457, + LANG_COMMAND_NPC_SHOWLOOT_MONEY = 35458, + LANG_COMMAND_NPC_SHOWLOOT_ITEMS = 35459, + LANG_COMMAND_NPC_SHOWLOOT_ENTRY = 35460, + LANG_COMMAND_NPC_SHOWLOOT_QUEST = 35461 + }; #endif diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 0616f12b7..28da07703 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -24,6 +24,7 @@ #include "GridNotifiers.h" #include "GridNotifiersImpl.h" #include "Language.h" +#include "LootMgr.h" #include "MapMgr.h" #include "ObjectMgr.h" #include "Pet.h" @@ -201,6 +202,7 @@ public: { "follow", npcFollowCommandTable }, { "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, 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 } }; @@ -1490,6 +1492,50 @@ public: return true; } + + static void ShowLootEntry(ChatHandler* handler, LootItem const& item) + { + ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item.itemid); + std::string name = itemTemplate ? itemTemplate->Name1 : "Unknown item"; + if (itemTemplate) + if (ItemLocale const* il = sObjectMgr->GetItemLocale(item.itemid)) + ObjectMgr::GetLocaleString(il->Name, handler->GetSessionDbLocaleIndex(), name); + + uint32 color = ItemQualityColors[itemTemplate ? itemTemplate->Quality : uint32(ITEM_QUALITY_POOR)]; + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_ENTRY, item.count, color, item.itemid, name, item.itemid); + } + + static bool HandleNpcShowLootCommand(ChatHandler* handler) + { + Creature* creatureTarget = handler->getSelectedCreature(); + if (!creatureTarget || creatureTarget->IsPet()) + { + handler->SendErrorMessage(LANG_SELECT_CREATURE); + return false; + } + + Loot const& loot = creatureTarget->loot; + if (!creatureTarget->isDead() || (loot.empty() && loot.quest_items.empty())) + { + handler->SendErrorMessage(LANG_COMMAND_NOT_DEAD_OR_NO_LOOT, creatureTarget->GetName()); + return false; + } + + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_HEADER, creatureTarget->GetName(), creatureTarget->GetEntry()); + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_MONEY, loot.gold / GOLD, (loot.gold % GOLD) / SILVER, loot.gold % SILVER); + + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_ITEMS, loot.items.size()); + for (LootItem const& item : loot.items) + if (!item.is_looted) + ShowLootEntry(handler, item); + + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_QUEST, loot.quest_items.size()); + for (LootItem const& item : loot.quest_items) + if (!item.is_looted) + ShowLootEntry(handler, item); + + return true; + } }; void AddSC_npc_commandscript() From f5078581397f2b3b8ec1c1017d2185cf6242f680 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 21:03:24 +0000 Subject: [PATCH 19/47] chore(DB): import pending files Referenced commit(s): 8cca034053ddd32dd08d4021b356dee54f12d111 --- .../rev_1782657620390367800.sql => db_world/2026_07_04_06.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1782657620390367800.sql => db_world/2026_07_04_06.sql} (98%) diff --git a/data/sql/updates/pending_db_world/rev_1782657620390367800.sql b/data/sql/updates/db_world/2026_07_04_06.sql similarity index 98% rename from data/sql/updates/pending_db_world/rev_1782657620390367800.sql rename to data/sql/updates/db_world/2026_07_04_06.sql index bd6c8f820..ea5663e28 100644 --- a/data/sql/updates/pending_db_world/rev_1782657620390367800.sql +++ b/data/sql/updates/db_world/2026_07_04_06.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_05 -> 2026_07_04_06 -- -- Issue #18038: add .npc showloot command entry and matching acore_string rows. DELETE FROM `command` WHERE `name` = 'npc showloot'; From 971a3f46f780b204d9c1123d10d1b698d4e0dba9 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:11:25 -0300 Subject: [PATCH 20/47] fix(Scripts/Ulduar): restrict Molgheim Rune of Death to players (#26470) Co-authored-by: Claude Opus 4.8 --- .../scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp index 3fcd245cb..c0a63ed30 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp @@ -516,9 +516,7 @@ struct boss_runemaster_molgeim : public ScriptedAI events.RescheduleEvent(EVENT_SHIELD_OF_RUNES, 27s, 34s); break; case EVENT_RUNE_OF_DEATH: - if (Unit* target = SelectTarget(SelectTargetMethod::Random)) - me->CastSpell(target, SPELL_RUNE_OF_DEATH, true); - + DoCastRandomTarget(SPELL_RUNE_OF_DEATH, 0, 0.0f, true, true); Talk(SAY_MOLGEIM_RUNE_DEATH); events.Repeat(30s, 40s); break; From b79f7c33129c571e5c9b3c89ae88a9914b68f15d Mon Sep 17 00:00:00 2001 From: Yehonal <147092+Yehonal@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:19:30 +0200 Subject: [PATCH 21/47] fix(Core/Movement): guard empty taxi flight paths (#26468) --- src/server/game/Movement/MotionMaster.cpp | 13 +++- .../WaypointMovementGenerator.cpp | 60 ++++++++++++------- .../WaypointMovementGenerator.h | 2 +- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/server/game/Movement/MotionMaster.cpp b/src/server/game/Movement/MotionMaster.cpp index 0801edecf..aba6e2c7b 100644 --- a/src/server/game/Movement/MotionMaster.cpp +++ b/src/server/game/Movement/MotionMaster.cpp @@ -28,6 +28,7 @@ #include "Log.h" #include "MoveSpline.h" #include "MoveSplineInit.h" +#include "Player.h" #include "PointMovementGenerator.h" #include "RandomMovementGenerator.h" #include "TargetedMovementGenerator.h" @@ -837,7 +838,17 @@ void MotionMaster::MoveTaxiFlight(uint32 path, uint32 pathnode) { LOG_DEBUG("movement.motionmaster", "{} taxi to (Path {} node {})", _owner->GetName(), path, pathnode); FlightPathMovementGenerator* mgen = new FlightPathMovementGenerator(pathnode); - mgen->LoadPath(_owner->ToPlayer()); + Player* player = _owner->ToPlayer(); + if (!mgen->LoadPath(player)) + { + LOG_ERROR("movement.motionmaster", "{} failed to build taxi path (Path {} node {}), clearing taxi destinations", + _owner->GetName(), path, pathnode); + player->m_taxi.ClearTaxiDestinations(); + player->Dismount(); + delete mgen; + return; + } + Mutate(mgen, MOTION_SLOT_CONTROLLED); } else diff --git a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp index c40d5a0d0..27c2cc45b 100644 --- a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp @@ -568,45 +568,56 @@ bool IsNodeIncludedInShortenedPath(TaxiPathNodeEntry const* p1, TaxiPathNodeEntr return p1->mapid != p2->mapid || std::pow(p1->x - p2->x, 2) + std::pow(p1->y - p2->y, 2) > SKIP_SPLINE_POINT_DISTANCE_SQ; } -void FlightPathMovementGenerator::LoadPath(Player* player) +bool FlightPathMovementGenerator::LoadPath(Player* player) { + i_path.clear(); _pointsForPathSwitch.clear(); + auto fail = [&]() + { + i_path.clear(); + _pointsForPathSwitch.clear(); + return false; + }; + std::deque const& taxi = player->m_taxi.GetPath(); float discount = player->GetReputationPriceDiscount(player->m_taxi.GetFlightMasterFactionTemplate()); for (uint32 src = 0, dst = 1; dst < taxi.size(); src = dst++) { uint32 path, cost; sObjectMgr->GetTaxiPath(taxi[src], taxi[dst], path, cost); - if (path > sTaxiPathNodesByPath.size()) - { - return; - } + if (path >= sTaxiPathNodesByPath.size()) + return fail(); TaxiPathNodeList const& nodes = sTaxiPathNodesByPath[path]; - if (!nodes.empty()) + if (nodes.empty()) + return fail(); + + TaxiPathNodeEntry const* start = nodes[0]; + TaxiPathNodeEntry const* end = nodes[nodes.size() - 1]; + bool passedPreviousSegmentProximityCheck = false; + bool addedPathNode = false; + for (uint32 i = 0; i < nodes.size(); ++i) { - TaxiPathNodeEntry const* start = nodes[0]; - TaxiPathNodeEntry const* end = nodes[nodes.size() - 1]; - bool passedPreviousSegmentProximityCheck = false; - for (uint32 i = 0; i < nodes.size(); ++i) + if (passedPreviousSegmentProximityCheck || !src || i_path.empty() || IsNodeIncludedInShortenedPath(i_path[i_path.size() - 1], nodes[i])) { - if (passedPreviousSegmentProximityCheck || !src || i_path.empty() || IsNodeIncludedInShortenedPath(i_path[i_path.size() - 1], nodes[i])) + if ((!src || (IsNodeIncludedInShortenedPath(start, nodes[i]) && i >= 2)) && + (dst == taxi.size() - 1 || (IsNodeIncludedInShortenedPath(end, nodes[i]) && i < nodes.size() - 1))) { - if ((!src || (IsNodeIncludedInShortenedPath(start, nodes[i]) && i >= 2)) && - (dst == taxi.size() - 1 || (IsNodeIncludedInShortenedPath(end, nodes[i]) && i < nodes.size() - 1))) - { - passedPreviousSegmentProximityCheck = true; - i_path.push_back(nodes[i]); - } - } - else - { - i_path.pop_back(); - --_pointsForPathSwitch.back().PathIndex; + passedPreviousSegmentProximityCheck = true; + i_path.push_back(nodes[i]); + addedPathNode = true; } } + else + { + i_path.pop_back(); + --_pointsForPathSwitch.back().PathIndex; + } } + if (!addedPathNode || i_path.empty()) + return fail(); + _pointsForPathSwitch.push_back({ uint32(i_path.size() - 1), int32(ceil(cost * discount)) }); } @@ -630,6 +641,11 @@ void FlightPathMovementGenerator::LoadPath(Player* player) else i_currentNode = 0; } + + if (i_path.empty()) + return fail(); + + return true; } void FlightPathMovementGenerator::DoInitialize(Player* player) diff --git a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h index 12479820a..66fd14162 100644 --- a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h +++ b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h @@ -108,7 +108,7 @@ class FlightPathMovementGenerator : public MovementGeneratorMedium< Player, Flig _endMapId = 0; _preloadTargetNode = 0; } - void LoadPath(Player* player); + bool LoadPath(Player* player); void DoInitialize(Player*); void DoReset(Player*); void DoFinalize(Player*); From 2ca73b789f34522ed632b67629a3491ca4ad0501 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:37:06 -0300 Subject: [PATCH 22/47] fix(Scripts/Ulduar): keep Freya's loot chest from despawning early (#26476) Co-authored-by: Machiavelli Co-authored-by: Claude Opus 4.8 --- .../Northrend/Ulduar/Ulduar/boss_freya.cpp | 17 ++++++++++------- .../scripts/Northrend/Ulduar/Ulduar/ulduar.h | 2 -- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index ab460a785..4b9819841 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -293,14 +293,17 @@ struct boss_freya : public BossAI ++_elderCount; } - uint32 chestId = RAID_MODE(GO_FREYA_CHEST, GO_FREYA_CHEST_HERO); - chestId -= 2 * _elderCount; // offset - - if (GameObject* go = me->SummonGameObject(chestId, 2345.61f, -71.20f, 425.104f, 3.0f, 0, 0, 0, 0, 0)) + // Summon the chest via spell so it is a wild object not owned by Freya, + // otherwise it despawns with her when she teleports out. The spell is + // chosen by raid size and how many Elders empowered her. + static constexpr uint32 summonChestSpell[2][4] = { - go->ReplaceAllGameObjectFlags((GameObjectFlags)0); - go->SetLootRecipient(me->GetMap()); - } + // 0 Elder, 1 Elder, 2 Elder, 3 Elder + { 62950, 62952, 62953, 62954 }, // 10-man + { 62955, 62956, 62957, 62958 } // 25-man + }; + + me->CastSpell(me, summonChestSpell[me->GetMap()->Is25ManRaid() ? 1 : 0][_elderCount], true); // Defeat credit me->CastSpell(me, 65074, true); // credit diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h index abf79d49d..314c174e6 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h @@ -233,8 +233,6 @@ enum UlduarGameObjects GO_HODIR_CHEST_NORMAL_HERO = 194308, GO_HODIR_CHEST_HARD = 194200, GO_HODIR_CHEST_HARD_HERO = 194201, - GO_FREYA_CHEST = 194330, // Normal, -2 - elder offset - GO_FREYA_CHEST_HERO = 194331, // Hero, -2 - elder offset GO_MIMIRON_CHEST = 194789, GO_MIMIRON_CHEST_HARD = 194957, GO_MIMIRON_CHEST_HERO = 194956, From c5d2632e6fe8966ac81c7122ae5ef3c7032ef47f Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:54:22 -0300 Subject: [PATCH 23/47] fix(DB/Ulduar): make General Vezax immune to spell-haste debuffs (#26474) Co-authored-by: Claude Opus 4.8 --- .../pending_db_world/rev_1783205268156315800.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783205268156315800.sql diff --git a/data/sql/updates/pending_db_world/rev_1783205268156315800.sql b/data/sql/updates/pending_db_world/rev_1783205268156315800.sql new file mode 100644 index 000000000..4c54a14e9 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783205268156315800.sql @@ -0,0 +1,11 @@ +-- +-- General Vezax (33271) must resist spell-haste debuffs (Curse of Tongues, Mind-numbing Poison, +-- Slow, core hound Lava Breath). They inflate his Shadow Crash / Searing Flames cast times and +-- trivialize the encounter. He already uses shared CC set -287; a single creature can reference only +-- one immunity set, so give him a dedicated superset that keeps -287's immunities and adds +-- aura 216 (HASTE_SPELLS). Other -287 users are left untouched. +DELETE FROM `creature_immunities` WHERE `ID`=-427; +INSERT INTO `creature_immunities` (`ID`, `SchoolMask`, `DispelTypeMask`, `MechanicsMask`, `Effects`, `Auras`, `ImmuneAoE`, `ImmuneChain`, `Comment`) VALUES +(-427, 0, 0, 1234599678, '98,114,124,144,145', '11,216', 0, 0, 'General Vezax: -287 (CC/knockback/taunt, auras=11(MOD_TAUNT)) + auras=216(HASTE_SPELLS) so cast-time slows do not trivialize the fight'); + +UPDATE `creature_template` SET `CreatureImmunitiesId`=-427 WHERE `entry`=33271; From 0ef916fdf5db8d8d25480f5e4f293b4ee8a43abe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Jul 2026 09:55:36 +0000 Subject: [PATCH 24/47] chore(DB): import pending files Referenced commit(s): c5d2632e6fe8966ac81c7122ae5ef3c7032ef47f --- .../rev_1783205268156315800.sql => db_world/2026_07_05_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783205268156315800.sql => db_world/2026_07_05_00.sql} (95%) diff --git a/data/sql/updates/pending_db_world/rev_1783205268156315800.sql b/data/sql/updates/db_world/2026_07_05_00.sql similarity index 95% rename from data/sql/updates/pending_db_world/rev_1783205268156315800.sql rename to data/sql/updates/db_world/2026_07_05_00.sql index 4c54a14e9..afbb48d66 100644 --- a/data/sql/updates/pending_db_world/rev_1783205268156315800.sql +++ b/data/sql/updates/db_world/2026_07_05_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_04_06 -> 2026_07_05_00 -- -- General Vezax (33271) must resist spell-haste debuffs (Curse of Tongues, Mind-numbing Poison, -- Slow, core hound Lava Breath). They inflate his Shadow Crash / Searing Flames cast times and From 523bd83ad6b458900f471242e78c5be9aab67332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=90=E5=A6=82=E4=BB=99?= <18535853+PkllonG@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:18:57 +0800 Subject: [PATCH 25/47] fix(Scripts/Ulduar): Change the spellID for the Freya summoning treasure chest (#26483) --- src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index 4b9819841..216aa247b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -296,11 +296,12 @@ struct boss_freya : public BossAI // Summon the chest via spell so it is a wild object not owned by Freya, // otherwise it despawns with her when she teleports out. The spell is // chosen by raid size and how many Elders empowered her. + // Order intentionally differs from TC/cMaNGOS to match AC's even/odd chest-loot grouping. static constexpr uint32 summonChestSpell[2][4] = { // 0 Elder, 1 Elder, 2 Elder, 3 Elder - { 62950, 62952, 62953, 62954 }, // 10-man - { 62955, 62956, 62957, 62958 } // 25-man + { 62957, 62955, 62953, 62950 }, // 10-man + { 62958, 62956, 62954, 62952 } // 25-man }; me->CastSpell(me, summonChestSpell[me->GetMap()->Is25ManRaid() ? 1 : 0][_elderCount], true); From fe82e9faf8736642f0844f2bc65f6003dfa73975 Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:52:41 -0500 Subject: [PATCH 26/47] refactor(Core/LFG): Reduce memory allocations in LFG hot paths (#25064) Co-authored-by: blinkysc Co-authored-by: sudlud --- src/server/game/DungeonFinding/LFGMgr.cpp | 99 +++++++++------------ src/server/game/DungeonFinding/LFGMgr.h | 11 ++- src/server/game/DungeonFinding/LFGQueue.cpp | 2 +- 3 files changed, 52 insertions(+), 60 deletions(-) diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 647284322..d3505b773 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -826,7 +826,6 @@ namespace lfg return; } - std::string debugNames = ""; if (grp) // Begin rolecheck { // Create new rolecheck @@ -857,9 +856,6 @@ namespace lfg if (!isContinue) SetSelectedDungeons(pguid, dungeons); roleCheck.roles[pguid] = 0; - if (!debugNames.empty()) - debugNames.append(", "); - debugNames.append(plrg->GetName()); } } // Update leader role @@ -886,16 +882,7 @@ namespace lfg player->GetSession()->SendLfgUpdatePlayer(LfgUpdateData(LFG_UPDATETYPE_JOIN_QUEUE, dungeons, comment)); SetState(guid, LFG_STATE_QUEUED); SetRoles(guid, roles); - debugNames.append(player->GetName()); } - - /*if (sLog->ShouldLog(LOG_FILTER_LFG, LOG_LEVEL_DEBUG)) - { - std::ostringstream o; - o << "LFGMgr::Join: [" << guid << "] joined (" << (grp ? "group" : "player") << ") Members: " << debugNames.c_str() - << ". Dungeons (" << uint32(dungeons.size()) << "): " << ConcatenateDungeons(dungeons); - LOG_DEBUG("lfg", "{}", o.str()); - }*/ } void LFGMgr::ToggleTesting() @@ -1099,10 +1086,7 @@ namespace lfg int32 spellDamage, spellHeal; uint32 dungeonId, encounterMask, maxPower; uint32 deletedCounter, groupCounter, playerCounter; - ByteBuffer buffer_deleted, buffer_groups, buffer_players; std::string emptyComment; - GuidSet deletedGroups, deletedGroupsToErase; - RBInternalInfoMap copy; for (uint8 team = 0; team < 2; ++team) { @@ -1207,18 +1191,18 @@ namespace lfg } } - copy.clear(); - copy = currInternalInfoMap; // will be saved as prev at the end + _rbCopy.clear(); + _rbCopy = currInternalInfoMap; // will be saved as prev at the end // compare prev with curr to build difference packet deletedCounter = 0; groupCounter = 0; playerCounter = 0; - buffer_deleted.clear(); - buffer_groups.clear(); - buffer_players.clear(); - deletedGroups.clear(); - deletedGroupsToErase.clear(); + _rbBufferDeleted.clear(); + _rbBufferGroups.clear(); + _rbBufferPlayers.clear(); + _rbDeletedGroups.clear(); + _rbDeletedGroupsToErase.clear(); RBInternalInfoMap& prevInternalInfoMap = RBInternalInfoStorePrev[team][dungeonId]; RBInternalInfoMap::iterator iter, iterTmp; @@ -1228,50 +1212,51 @@ namespace lfg if (iter == currInternalInfoMap.end()) // was -> isn't { if (sitr->second.isGroupLeader) - deletedGroups.insert(sitr->second.groupGuid); + _rbDeletedGroups.insert(sitr->second.groupGuid); + ++deletedCounter; - buffer_deleted << sitr->second.guid; + _rbBufferDeleted << sitr->second.guid; } else // was -> is { if (sitr->second.isGroupLeader) // was a leader { if (!iter->second.isGroupLeader) // leader -> no longer a leader - deletedGroups.insert(sitr->second.groupGuid); + _rbDeletedGroups.insert(sitr->second.groupGuid); else if (sitr->second.groupGuid != iter->second.groupGuid) // leader -> leader of another group { - deletedGroups.insert(sitr->second.groupGuid); - deletedGroupsToErase.insert(iter->second.groupGuid); + _rbDeletedGroups.insert(sitr->second.groupGuid); + _rbDeletedGroupsToErase.insert(iter->second.groupGuid); ++groupCounter; - RBPacketAppendGroup(iter->second, buffer_groups); + RBPacketAppendGroup(iter->second, _rbBufferGroups); } else if (sitr->second.comment != iter->second.comment || sitr->second.encounterMask != iter->second.encounterMask || sitr->second.instanceGuid != iter->second.instanceGuid) // leader -> nothing changed { ++groupCounter; - RBPacketAppendGroup(iter->second, buffer_groups); + RBPacketAppendGroup(iter->second, _rbBufferGroups); } } else if (iter->second.isGroupLeader) // wasn't a leader -> is a leader { - deletedGroupsToErase.insert(iter->second.groupGuid); + _rbDeletedGroupsToErase.insert(iter->second.groupGuid); ++groupCounter; - RBPacketAppendGroup(iter->second, buffer_groups); + RBPacketAppendGroup(iter->second, _rbBufferGroups); } if (!iter->second._online) // if offline, copy previous stats (itemLevel, talents, area, etc.) { - iterTmp = copy.find(sitr->first); // copied container is for building a full packet, so modify it there (currInternalInfoMap is erased) + iterTmp = _rbCopy.find(sitr->first); // copied container is for building a full packet, so modify it there (currInternalInfoMap is erased) iterTmp->second.CopyStats(sitr->second); if (!sitr->second.PlayerSameAs(iterTmp->second)) // player info changed { ++playerCounter; - RBPacketAppendPlayer(iterTmp->second, buffer_players); + RBPacketAppendPlayer(iterTmp->second, _rbBufferPlayers); } } else if (!sitr->second.PlayerSameAs(iter->second)) // player info changed { ++playerCounter; - RBPacketAppendPlayer(iter->second, buffer_players); + RBPacketAppendPlayer(iter->second, _rbBufferPlayers); } currInternalInfoMap.erase(iter); } @@ -1281,38 +1266,38 @@ namespace lfg { if (sitr->second.isGroupLeader) { - deletedGroupsToErase.insert(sitr->second.groupGuid); + _rbDeletedGroupsToErase.insert(sitr->second.groupGuid); ++groupCounter; - RBPacketAppendGroup(sitr->second, buffer_groups); + RBPacketAppendGroup(sitr->second, _rbBufferGroups); } ++playerCounter; - RBPacketAppendPlayer(sitr->second, buffer_players); + RBPacketAppendPlayer(sitr->second, _rbBufferPlayers); } - if (!deletedGroupsToErase.empty()) + if (!_rbDeletedGroupsToErase.empty()) { - for (ObjectGuid const& toErase : deletedGroupsToErase) + for (ObjectGuid const& toErase : _rbDeletedGroupsToErase) { - deletedGroups.erase(toErase); + _rbDeletedGroups.erase(toErase); } } - if (!deletedGroups.empty()) + if (!_rbDeletedGroups.empty()) { - for (ObjectGuid const& deletedGroup : deletedGroups) + for (ObjectGuid const& deletedGroup : _rbDeletedGroups) { ++deletedCounter; - buffer_deleted << deletedGroup; + _rbBufferDeleted << deletedGroup; } } WorldPacket differencePacket(SMSG_UPDATE_LFG_LIST, 1000); - RBPacketBuildDifference(differencePacket, dungeonId, deletedCounter, buffer_deleted, groupCounter, buffer_groups, playerCounter, buffer_players); + RBPacketBuildDifference(differencePacket, dungeonId, deletedCounter, _rbBufferDeleted, groupCounter, _rbBufferGroups, playerCounter, _rbBufferPlayers); WorldPacket fullPacket(SMSG_UPDATE_LFG_LIST, 1000); - RBPacketBuildFull(fullPacket, dungeonId, copy); + RBPacketBuildFull(fullPacket, dungeonId, _rbCopy); RBCacheStore[team][dungeonId] = fullPacket; - prevInternalInfoMap = copy; + prevInternalInfoMap = _rbCopy; currInternalInfoMap.clear(); if (entryInfoMap.empty()) @@ -1403,44 +1388,44 @@ namespace lfg buffer << (uint32)info.encounterMask; } - void LFGMgr::RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer& buffer_deleted, uint32 groupCounter, ByteBuffer& buffer_groups, uint32 playerCounter, ByteBuffer& buffer_players) + void LFGMgr::RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer const& bufferDeleted, uint32 groupCounter, ByteBuffer const& bufferGroups, uint32 playerCounter, ByteBuffer const& bufferPlayers) { differencePacket << (uint32)LFG_TYPE_RAID; differencePacket << (uint32)dungeonId; differencePacket << (uint8)1; differencePacket << (uint32)deletedCounter; - differencePacket.append(buffer_deleted); + differencePacket.append(bufferDeleted); differencePacket << (uint32)groupCounter; differencePacket << (uint32)0; - differencePacket.append(buffer_groups); + differencePacket.append(bufferGroups); differencePacket << (uint32)playerCounter; differencePacket << (uint32)0; - differencePacket.append(buffer_players); + differencePacket.append(bufferPlayers); } - void LFGMgr::RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap& infoMap) + void LFGMgr::RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap const& infoMap) { fullPacket << (uint32)LFG_TYPE_RAID; fullPacket << (uint32)dungeonId; fullPacket << (uint8)0; uint32 groupCounter = 0, playerCounter = 0; - ByteBuffer buffer_groups, buffer_players; + ByteBuffer bufferGroups, bufferPlayers; for (RBInternalInfoMap::const_iterator itr = infoMap.begin(); itr != infoMap.end(); ++itr) { if (itr->second.isGroupLeader) { ++groupCounter; - RBPacketAppendGroup(itr->second, buffer_groups); + RBPacketAppendGroup(itr->second, bufferGroups); } ++playerCounter; - RBPacketAppendPlayer(itr->second, buffer_players); + RBPacketAppendPlayer(itr->second, bufferPlayers); } fullPacket << (uint32)groupCounter; fullPacket << (uint32)0; - fullPacket.append(buffer_groups); + fullPacket.append(bufferGroups); fullPacket << (uint32)playerCounter; fullPacket << (uint32)0; - fullPacket.append(buffer_players); + fullPacket.append(bufferPlayers); } // pussywizard: diff --git a/src/server/game/DungeonFinding/LFGMgr.h b/src/server/game/DungeonFinding/LFGMgr.h index 2164de059..358c5802a 100644 --- a/src/server/game/DungeonFinding/LFGMgr.h +++ b/src/server/game/DungeonFinding/LFGMgr.h @@ -20,6 +20,7 @@ #include +#include "ByteBuffer.h" #include "DBCStructure.h" #include "Field.h" #include "LFG.h" @@ -562,8 +563,8 @@ namespace lfg void SendRaidBrowserJoinedPacket(Player* p, LfgDungeonSet& dungeons, std::string comment); void RBPacketAppendGroup(const RBInternalInfo& info, ByteBuffer& buffer); void RBPacketAppendPlayer(const RBInternalInfo& info, ByteBuffer& buffer); - void RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer& buffer_deleted, uint32 groupCounter, ByteBuffer& buffer_groups, uint32 playerCounter, ByteBuffer& buffer_players); - void RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap& infoMap); + void RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer const& bufferDeleted, uint32 groupCounter, ByteBuffer const& bufferGroups, uint32 playerCounter, ByteBuffer const& bufferPlayers); + void RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap const& infoMap); // LfgQueue /// Get last lfg state (NONE, DUNGEON or FINISHED_DUNGEON) @@ -627,6 +628,12 @@ namespace lfg uint32 lastProposalId; ///< pussywizard, store it here because of splitting LFGMgr update into tasks uint32 m_raidBrowserUpdateTimer[2]; ///< pussywizard uint32 m_raidBrowserLastUpdatedDungeonId[2]; ///< pussywizard: for 2 factions + ByteBuffer _rbBufferDeleted; + ByteBuffer _rbBufferGroups; + ByteBuffer _rbBufferPlayers; + GuidSet _rbDeletedGroups; + GuidSet _rbDeletedGroupsToErase; + RBInternalInfoMap _rbCopy; LfgQueueContainer QueuesStore; ///< Queues LfgCachedDungeonContainer CachedDungeonMapStore; ///< Stores all dungeons by groupType diff --git a/src/server/game/DungeonFinding/LFGQueue.cpp b/src/server/game/DungeonFinding/LFGQueue.cpp index a7890fc9f..b45d7a0c5 100644 --- a/src/server/game/DungeonFinding/LFGQueue.cpp +++ b/src/server/game/DungeonFinding/LFGQueue.cpp @@ -374,7 +374,7 @@ namespace lfg LfgDungeonSet temporal; LfgDungeonSet& dungeons = QueueDataStore[check.guids[i]].dungeons; std::set_intersection(proposalDungeons.begin(), proposalDungeons.end(), dungeons.begin(), dungeons.end(), std::inserter(temporal, temporal.begin())); - proposalDungeons = temporal; + std::swap(proposalDungeons, temporal); } if (proposalDungeons.empty()) From ca790ef4082f84cd583814d92956d493ad17a10f Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:23:51 -0300 Subject: [PATCH 27/47] fix(Core/Mail): load undelivered mails at login (#26486) Co-authored-by: Shauren Co-authored-by: Claude Fable 5 --- .../database/Database/Implementation/CharacterDatabase.cpp | 2 +- src/server/game/Handlers/CharacterHandler.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index ca1bfa5e3..be215d322 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -423,7 +423,7 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHAR_SOCIAL, "SELECT DISTINCT guid FROM character_social WHERE friend = ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHAR_OLD_CHARS, "SELECT guid, deleteInfos_Account FROM characters WHERE deleteDate IS NOT NULL AND deleteDate < ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_ARENA_TEAM_ID_BY_PLAYER_GUID, "SELECT arena_team_member.arenateamid FROM arena_team_member JOIN arena_team ON arena_team_member.arenateamid = arena_team.arenateamid WHERE guid = ? AND type = ? LIMIT 1", CONNECTION_SYNCH); - PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId FROM mail WHERE receiver = ? AND deliver_time <= ? ORDER BY id DESC", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId FROM mail WHERE receiver = ? ORDER BY id DESC", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_NEXT_MAIL_DELIVERYTIME, "SELECT MIN(deliver_time) FROM mail WHERE receiver = ? AND deliver_time > ? AND (checked & 1) = 0 LIMIT 1", CONNECTION_SYNCH); PrepareStatement(CHAR_DEL_CHAR_AURA_FROZEN, "DELETE FROM character_aura WHERE spell = 9454 AND guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHAR_INVENTORY_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM character_inventory ci INNER JOIN item_instance ii ON ii.guid = ci.item WHERE itemEntry = ?", CONNECTION_SYNCH); diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index a13ea684c..d00e00aba 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -129,7 +129,6 @@ bool LoginQueryHolder::Initialize() stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL); stmt->SetData(0, lowGuid); - stmt->SetData(1, uint32(GameTime::GetGameTime().count())); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAILS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAILITEMS); From 8900c564ececa35182c1931798a716a9fdcf8861 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:26:36 -0300 Subject: [PATCH 28/47] fix(Scripts/Commands): stop .mail return sending empty mails for online players (#26485) Co-authored-by: Claude Fable 5 --- src/server/scripts/Commands/cs_mail.cpp | 212 +++++++++++++----------- 1 file changed, 113 insertions(+), 99 deletions(-) diff --git a/src/server/scripts/Commands/cs_mail.cpp b/src/server/scripts/Commands/cs_mail.cpp index 6f622e2b3..3fcf3a2c8 100644 --- a/src/server/scripts/Commands/cs_mail.cpp +++ b/src/server/scripts/Commands/cs_mail.cpp @@ -243,37 +243,119 @@ public: Player* player = target.GetConnectedPlayer(); - // Run the same script hook as the client return handler, failing early before any deletions + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + // Collect attachments before the script-hook check and any deletion, so a hook + // veto can still abort the command with nothing changed. + // mail_items is flushed to the DB on every item take, so it is authoritative even + // for online players; mails the session never loaded (expired at login, delivered + // mid-login, inserted externally) are returned with their items too. Same query + // shape as CHAR_SEL_MAILITEMS (LEFT JOIN to handle dangling mail_items) and same + // logic as Player::_LoadMailedItem. Items the session already has loaded are + // reused and stay owned by the session; the bool marks objects loaded (and owned) + // here. + std::vector> attachments; + + QueryResult itemResult = CharacterDatabase.Query( + "SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments," + " randomPropertyId, durability, playedTime, text, mi.item_guid, itemEntry, ii.owner_guid" + " FROM mail_items mi LEFT JOIN item_instance ii ON mi.item_guid = ii.guid" + " WHERE mi.mail_id = {}", mailId); + + if (itemResult) + { + do + { + Field* itemFields = itemResult->Fetch(); + uint32 itemGuid = itemFields[11].Get(); + uint32 itemEntry = itemFields[12].Get(); + + // Prefer the item object the session already has loaded over creating a duplicate + if (Item* item = player ? player->GetMItem(itemGuid) : nullptr) + { + attachments.emplace_back(item, false); + continue; + } + + // Handle dangling mail_items (missing item_instance) + if (!itemEntry) + { + LOG_ERROR("misc", "cs_mail: Mail #{} has dangling mail_items row for item_guid {}. Cleaning up.", mailId, itemGuid); + + CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); + delStmt->SetData(0, itemGuid); + trans->Append(delStmt); + continue; + } + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry); + if (!proto) + { + LOG_ERROR("misc", "cs_mail: Mail #{} has unknown item (entry: {}, guid: {}). Cleaning up.", mailId, itemEntry, itemGuid); + + CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); + delStmt->SetData(0, itemGuid); + trans->Append(delStmt); + + // The mail is going away, so drop the unloadable item_instance row too + Item::DeleteFromDB(trans, itemGuid); + continue; + } + + Item* item = NewItemOrBag(proto); + ObjectGuid ownerGuid = itemFields[13].Get() + ? ObjectGuid::Create(itemFields[13].Get()) + : ObjectGuid::Empty; + + if (!item->LoadFromDB(itemGuid, ownerGuid, itemFields, itemEntry)) + { + LOG_ERROR("misc", "cs_mail: Item (GUID: {}) in mail #{} failed to load. Cleaning up.", itemGuid, mailId); + + CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); + delStmt->SetData(0, itemGuid); + trans->Append(delStmt); + + // Queue the row cleanup on the shared transaction instead of executing it + // immediately; SaveToDB in ITEM_REMOVED state also frees the object + item->FSetState(ITEM_REMOVED); + item->SaveToDB(trans); + continue; + } + + attachments.emplace_back(item, true); + } while (itemResult->NextRow()); + } + + // Run the same script hook as the client return handler for online targets, + // covering DB-loaded attachments as well, before any deletion if (player) { - Mail* m = player->GetMail(mailId); - if (m) - { - ObjectGuid senderGuid = ObjectGuid(HighGuid::Player, sender); + ObjectGuid senderGuid = ObjectGuid(HighGuid::Player, sender); + bool blocked = false; - if (m->HasItems()) - { - for (auto const& itemInfo : m->items) - { - Item* item = player->GetMItem(itemInfo.item_guid); - if (item && !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, item)) - { - handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED); - return true; - } - } - } - else if (!sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, nullptr)) - { - handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED); - return true; - } + if (attachments.empty()) + blocked = !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, nullptr); + + for (auto const& [item, loadedFromDb] : attachments) + { + if (blocked) + break; + + blocked = !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, item); + } + + if (blocked) + { + for (auto const& [item, loadedFromDb] : attachments) + if (loadedFromDb) + delete item; + + handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED); + return true; } } // Same logic as WorldSession::HandleReturnToSender - CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); stmt->SetData(0, mailId); trans->Append(stmt); @@ -288,87 +370,19 @@ public: if (player) { - // Online: same logic as WorldSession::HandleReturnToSender - // Get pointer before RemoveMail (which removes from deque but does not delete the object) + // Drop the mail from the session's loaded state if present. + // RemoveMail only erases from the deque, it does not delete the object. Mail* m = player->GetMail(mailId); - player->RemoveMail(mailId); - - if (m && m->HasItems()) - { - for (auto const& itemInfo : m->items) - { - if (Item* item = player->GetMItem(itemInfo.item_guid)) - draft.AddItem(item); - - player->RemoveMItem(itemInfo.item_guid); - } - } - delete m; } - else + + for (auto const& [item, loadedFromDb] : attachments) { - // Offline: load Item* objects from DB using same query shape as CHAR_SEL_MAILITEMS - // (LEFT JOIN to handle dangling mail_items) and same logic as Player::_LoadMailedItem - QueryResult itemResult = CharacterDatabase.Query( - "SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments," - " randomPropertyId, durability, playedTime, text, mi.item_guid, itemEntry, ii.owner_guid" - " FROM mail_items mi LEFT JOIN item_instance ii ON mi.item_guid = ii.guid" - " WHERE mi.mail_id = {}", mailId); + if (!loadedFromDb) + player->RemoveMItem(item->GetGUID().GetCounter()); - if (itemResult) - { - do - { - Field* itemFields = itemResult->Fetch(); - uint32 itemGuid = itemFields[11].Get(); - uint32 itemEntry = itemFields[12].Get(); - - // Handle dangling mail_items (missing item_instance) - if (!itemEntry) - { - LOG_ERROR("misc", "cs_mail: Mail #{} has dangling mail_items row for item_guid {}. Cleaning up.", mailId, itemGuid); - - CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); - delStmt->SetData(0, itemGuid); - trans->Append(delStmt); - continue; - } - - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry); - if (!proto) - { - LOG_ERROR("misc", "cs_mail: Mail #{} has unknown item (entry: {}, guid: {}). Cleaning up.", mailId, itemEntry, itemGuid); - - CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); - delStmt->SetData(0, itemGuid); - trans->Append(delStmt); - continue; - } - - Item* item = NewItemOrBag(proto); - ObjectGuid ownerGuid = itemFields[13].Get() - ? ObjectGuid::Create(itemFields[13].Get()) - : ObjectGuid::Empty; - - if (!item->LoadFromDB(itemGuid, ownerGuid, itemFields, itemEntry)) - { - LOG_ERROR("misc", "cs_mail: Item (GUID: {}) in mail #{} failed to load. Cleaning up.", itemGuid, mailId); - - CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); - delStmt->SetData(0, itemGuid); - trans->Append(delStmt); - - item->FSetState(ITEM_REMOVED); - CharacterDatabaseTransaction nullTrans = CharacterDatabaseTransaction(nullptr); - item->SaveToDB(nullTrans); - return true; - } - - draft.AddItem(item); - } while (itemResult->NextRow()); - } + draft.AddItem(item); } uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(ObjectGuid(HighGuid::Player, receiver)); From 0b1808057f2f7ba91d80d601ecdd705d09632d1e Mon Sep 17 00:00:00 2001 From: Rocco Silipo <108557877+Rorschach91@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:49:34 +0200 Subject: [PATCH 29/47] fix(DB/Formation): Change wrong GroupAI in Molten Colossus formation. (#26471) --- data/sql/updates/pending_db_world/rev_1783198828.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783198828.sql diff --git a/data/sql/updates/pending_db_world/rev_1783198828.sql b/data/sql/updates/pending_db_world/rev_1783198828.sql new file mode 100644 index 000000000..db5f7d945 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783198828.sql @@ -0,0 +1,3 @@ + +-- Set Correct GroupAI (it was 5). +UPDATE `creature_formations` SET `groupAI` = 3 WHERE `leaderGUID` = 136057; From 8c82af0d57496258cd4899eb18f8f0b42d6fa8bb Mon Sep 17 00:00:00 2001 From: Rocco Silipo <108557877+Rorschach91@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:49:39 +0200 Subject: [PATCH 30/47] fix(DB/SAI): Peacekeeper Security Suit now goes in melee range on engage. (#26482) --- data/sql/updates/pending_db_world/rev_1783248002.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783248002.sql diff --git a/data/sql/updates/pending_db_world/rev_1783248002.sql b/data/sql/updates/pending_db_world/rev_1783248002.sql new file mode 100644 index 000000000..2fe755ea9 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783248002.sql @@ -0,0 +1,3 @@ + +-- Set Cast Flag for Net Spell. +UPDATE `smart_scripts` SET `action_param2` = 12 WHERE (`entryorguid` = 6230) AND (`source_type` = 0) AND (`id` IN (0)); From dfde5c2d9463e43c9f0e3320a6c706217e31c88a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Jul 2026 17:50:49 +0000 Subject: [PATCH 31/47] chore(DB): import pending files Referenced commit(s): 8c82af0d57496258cd4899eb18f8f0b42d6fa8bb --- .../rev_1783198828.sql => db_world/2026_07_05_01.sql} | 1 + .../rev_1783248002.sql => db_world/2026_07_05_02.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1783198828.sql => db_world/2026_07_05_01.sql} (71%) rename data/sql/updates/{pending_db_world/rev_1783248002.sql => db_world/2026_07_05_02.sql} (77%) diff --git a/data/sql/updates/pending_db_world/rev_1783198828.sql b/data/sql/updates/db_world/2026_07_05_01.sql similarity index 71% rename from data/sql/updates/pending_db_world/rev_1783198828.sql rename to data/sql/updates/db_world/2026_07_05_01.sql index db5f7d945..4658d5005 100644 --- a/data/sql/updates/pending_db_world/rev_1783198828.sql +++ b/data/sql/updates/db_world/2026_07_05_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_05_00 -> 2026_07_05_01 -- Set Correct GroupAI (it was 5). UPDATE `creature_formations` SET `groupAI` = 3 WHERE `leaderGUID` = 136057; diff --git a/data/sql/updates/pending_db_world/rev_1783248002.sql b/data/sql/updates/db_world/2026_07_05_02.sql similarity index 77% rename from data/sql/updates/pending_db_world/rev_1783248002.sql rename to data/sql/updates/db_world/2026_07_05_02.sql index 2fe755ea9..a771ac5e7 100644 --- a/data/sql/updates/pending_db_world/rev_1783248002.sql +++ b/data/sql/updates/db_world/2026_07_05_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_05_01 -> 2026_07_05_02 -- Set Cast Flag for Net Spell. UPDATE `smart_scripts` SET `action_param2` = 12 WHERE (`entryorguid` = 6230) AND (`source_type` = 0) AND (`id` IN (0)); From 55de765eb4dd610c4fec37f313e80c87c95c92e1 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:03:10 +0200 Subject: [PATCH 32/47] =?UTF-8?q?fix(tools/codestyle):=20catch=20missing?= =?UTF-8?q?=20semicolon=20on=20inline=20INSERT=20statem=E2=80=A6=20(#26472?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++ apps/codestyle/codestyle-sql.py | 59 ++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index c2aea7a19..156a41753 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,10 @@ local.properties /vcpkg/ /vcpkg-ports/ +# Python bytecode cache +__pycache__/ +*.py[cod] + # ================== # diff --git a/apps/codestyle/codestyle-sql.py b/apps/codestyle/codestyle-sql.py index eb36033a1..c9c9556d6 100644 --- a/apps/codestyle/codestyle-sql.py +++ b/apps/codestyle/codestyle-sql.py @@ -210,6 +210,36 @@ def insert_delete_safety_check(file: io, file_path: str) -> None: error_handler = True results["INSERT & DELETE safety usage check"] = "Failed" +# Strip a trailing "-- ..." line comment while ignoring any "--" that appears +# inside a single- or double-quoted string literal (e.g. descriptions). +def strip_inline_comment(text: str) -> str: + in_single_quote = False + in_double_quote = False + index = 0 + while index < len(text): + char = text[index] + # Skip backslash-escaped characters inside string literals (e.g. \') + if char == '\\' and (in_single_quote or in_double_quote): + index += 2 + continue + if char == "'" and not in_double_quote: + in_single_quote = not in_single_quote + elif char == '"' and not in_single_quote: + in_double_quote = not in_double_quote + elif (char == '-' and index + 1 < len(text) and text[index + 1] == '-' + and not in_single_quote and not in_double_quote): + return text[:index].strip() + index += 1 + return text.strip() + +# Count how many parentheses are still open on a line, ignoring any that appear +# inside string literals. A positive result means a value tuple continues on the +# following line(s). +def open_paren_balance(text: str) -> int: + without_strings = re.sub(r"'(?:\\.|[^'])*'", "", text) + without_strings = re.sub(r'"(?:\\.|[^"])*"', "", without_strings) + return without_strings.count('(') - without_strings.count(')') + def semicolon_check(file: io, file_path: str) -> None: global error_handler, results @@ -255,8 +285,8 @@ def semicolon_check(file: io, file_path: str) -> None: if not stripped_line and not inside_values_block: continue - # Remove inline comments after SQL - stripped_line = stripped_line.split('--', 1)[0].strip() + # Remove inline comments after SQL (ignoring "--" inside string literals) + stripped_line = strip_inline_comment(stripped_line) if stripped_line.upper().startswith("SET") and not stripped_line.endswith(";"): print(f"❌ Missing semicolon in {file_path} at line {line_number}") @@ -266,10 +296,29 @@ def semicolon_check(file: io, file_path: str) -> None: if not query_open and any(keyword in stripped_line.upper() for keyword in ["SELECT", "INSERT", "UPDATE", "DELETE", "REPLACE"]): query_open = True - # Detect start of multi-line VALUES block - if any(kw in stripped_line.upper() for kw in ["INSERT", "REPLACE"]) and "VALUES" in stripped_line.upper(): - inside_values_block = True + # Detect start of a VALUES block + upper_line = stripped_line.upper() + if any(kw in upper_line for kw in ["INSERT", "REPLACE"]) and "VALUES" in upper_line: query_open = True # Ensure query is marked open too + # Look at whatever follows the VALUES keyword on this same line + tail = stripped_line[upper_line.rfind("VALUES") + len("VALUES"):].strip() + if not tail or tail.endswith(','): + # Multi-line VALUES block: value rows follow on subsequent lines + inside_values_block = True + elif open_paren_balance(stripped_line) > 0: + # A value tuple is still open (row split across lines, or the line + # ends with '('); leave the statement open so the terminator is + # validated once the tuple closes on a following line. + pass + elif tail.endswith(';'): + # Complete single-line insert + query_open = False + else: + # Inline insert whose value tuple(s) are complete on this same line + # but the statement is not terminated with a semicolon + print(f"❌ Missing semicolon in {file_path} at line {line_number}") + check_failed = True + query_open = False if inside_values_block: if not stripped_line: From fee9709b3e96f0ddf04276ba853983363c9ba9e9 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:43:21 -0300 Subject: [PATCH 33/47] feat(Core/AI): allow instance creatures to teleport to unreachable targets (#26459) Co-authored-by: Claude Fable 5 --- .../apps/worldserver/worldserver.conf.dist | 10 +++++++++ src/server/game/AI/CoreAI/UnitAI.cpp | 21 +++++++++++++++++++ src/server/game/World/WorldConfig.cpp | 1 + src/server/game/World/WorldConfig.h | 1 + 4 files changed, 33 insertions(+) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 7b16a07cb..dd6fe04a3 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -3184,6 +3184,16 @@ WaypointMovementStopTimeForPlayer = 120 NpcRegenHPIfTargetIsUnreachable = 1 +# Creature.Instance.TeleportToUnreachableTarget +# Description: In dungeons and raids, non-boss creatures that cannot path to their +# target teleport to it instead of evading or regenerating in place +# (retail-like behaviour). Bosses and player-controlled creatures are +# not affected. +# Default: 0 - (Disabled) +# 1 - (Enabled) + +Creature.Instance.TeleportToUnreachableTarget = 0 + # Creatures.CustomIDs # Description: The list of custom creatures with gossip dialogues hardcoded in core, # divided by "," without spaces. diff --git a/src/server/game/AI/CoreAI/UnitAI.cpp b/src/server/game/AI/CoreAI/UnitAI.cpp index dad8adebe..d08958a94 100644 --- a/src/server/game/AI/CoreAI/UnitAI.cpp +++ b/src/server/game/AI/CoreAI/UnitAI.cpp @@ -24,6 +24,7 @@ #include "SpellAuraEffects.h" #include "SpellInfo.h" #include "SpellMgr.h" +#include "World.h" void UnitAI::AttackStart(Unit* victim) { @@ -414,6 +415,26 @@ void UnitAI::EvadeTimerExpired() } } + // Retail-like: instance trash teleports to its unreachable target instead of evading + if (sWorld->getBoolConfig(CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET) + && creature->GetMap()->IsDungeon() + && !creature->IsDungeonBoss() && !creature->isWorldBoss() + && !creature->IsControlledByPlayer()) + { + if (ObjectGuid targetGuid = creature->GetCannotReachTarget()) + { + if (Unit* target = ObjectAccessor::GetUnit(*creature, targetGuid)) + { + if (target->IsAlive() && creature->IsEngagedBy(target)) + { + creature->NearTeleportTo(target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), target->GetOrientation()); + creature->SetCannotReachTarget(); + return; + } + } + } + } + if (creature->GetMap()->IsRaid()) { creature->GetCombatManager().ContinueEvadeRegen(); diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 1cc9cd997..5233d6e48 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -477,6 +477,7 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN, "OffhandCheckAtSpellUnlearn", true); SetConfigValue(CONFIG_CREATURE_REPOSITION_AGAINST_NPCS, "Creature.RepositionAgainstNpcs", true); + SetConfigValue(CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET, "Creature.Instance.TeleportToUnreachableTarget", false); SetConfigValue(CONFIG_CREATURE_STOP_FOR_PLAYER, "Creature.MovingStopTimeForPlayer", 180000); SetConfigValue(CONFIG_WATER_BREATH_TIMER, "WaterBreath.Timer", 180000, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value > 0; }, "> 0"); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index dd0707fce..7e3ef1319 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -75,6 +75,7 @@ enum ServerConfigs CONFIG_ARENA_QUEUE_ANNOUNCER_PLAYERONLY, CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN, CONFIG_CREATURE_REPOSITION_AGAINST_NPCS, + CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET, CONFIG_VMAP_INDOOR_CHECK, CONFIG_VMAP_ENABLE_LOS, CONFIG_VMAP_ENABLE_HEIGHT, From 28f803a8285db6f8446195486d3b0b7101529446 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 6 Jul 2026 06:00:39 -0300 Subject: [PATCH 34/47] fix(Scripts/Ulduar): fix Saronite Animus not engaging combat (#26501) --- .../Northrend/Ulduar/Ulduar/boss_general_vezax.cpp | 8 ++------ .../scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp | 7 +++++++ src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h | 1 + 3 files changed, 10 insertions(+), 6 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 849576a15..de0677dd4 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp @@ -65,7 +65,6 @@ enum VezaxNpcs { // NPC_VEZAX = 33271, // NPC_VEZAX_BUNNY = 33500, - NPC_SARONITE_ANIMUS = 33524, }; enum VezaxGOs @@ -368,11 +367,7 @@ struct npc_ulduar_saronite_animus : public ScriptedAI npc_ulduar_saronite_animus(Creature* creature) : ScriptedAI(creature) { _instance = creature->GetInstanceScript(); - if (_instance) - if (Creature* vezax = _instance->GetCreature(BOSS_VEZAX)) - vezax->AI()->JustSummoned(me); timer = 0; - me->SetInCombatWithZone(); } InstanceScript* _instance; @@ -389,7 +384,8 @@ struct npc_ulduar_saronite_animus : public ScriptedAI void UpdateAI(uint32 diff) override { - UpdateVictim(); + if (!UpdateVictim()) + return; timer += diff; if (timer >= 2000) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index b54ff8b97..9daa556ff 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -165,6 +165,12 @@ ObjectData const gameobjectData[] = { 0, 0 } }; +ObjectData const summonData[] = +{ + { NPC_SARONITE_ANIMUS, BOSS_VEZAX }, // summoned by a Saronite Vapor, not Vezax + { 0, 0 } +}; + BossBoundaryData const boundaries = { { BOSS_LEVIATHAN, new RectangleBoundary(130.0f, 450.0f, -170.0f, 110.0f) }, @@ -189,6 +195,7 @@ public: SetPersistentDataCount(MAX_PERSISTENT_DATA); LoadDoorData(doorData); LoadObjectData(creatureData, gameobjectData); + LoadSummonData(summonData); LoadBossBoundaries(boundaries); Initialize(); }; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h index 314c174e6..faaf83d21 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h @@ -172,6 +172,7 @@ enum UlduarNPCs NPC_THORIM = 32865, NPC_FREYA = 32906, NPC_VEZAX = 33271, + NPC_SARONITE_ANIMUS = 33524, NPC_SARA = 33134, NPC_YOGGSARON = 33288, NPC_BRAIN_OF_YOGG_SARON = 33890, From 4d5c23e1910ef04edbf538b11546f2032d18d799 Mon Sep 17 00:00:00 2001 From: zackchadwick <73787423+zackchadwick@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:36:04 -0400 Subject: [PATCH 35/47] fix(Core/Creature): Update Marksman Bova location and model (#26504) Co-authored-by: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> --- .../pending_db_world/rev_1783319870710449449.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783319870710449449.sql diff --git a/data/sql/updates/pending_db_world/rev_1783319870710449449.sql b/data/sql/updates/pending_db_world/rev_1783319870710449449.sql new file mode 100644 index 000000000..769c202a4 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783319870710449449.sql @@ -0,0 +1,10 @@ +-- Move Marksman Bova to the correct position and model. + +UPDATE `creature` SET `position_x` = -1721.4673, `position_y` = 5637.9883, `position_z` = 128.10652, `orientation` = 2.321287870407104492 WHERE `guid` = 85406 AND `id` = 25195; + +DELETE FROM `creature_template_model` WHERE `CreatureID` = 25195; +INSERT INTO `creature_template_model` (`CreatureID`, `Idx`, `CreatureDisplayID`, `DisplayScale`, `Probability`, `VerifiedBuild`) VALUES +(25195, 0, 18743, 1, 1, 51831), +(25195, 1, 18742, 1, 0, 51831), +(25195, 2, 18741, 1, 0, 51831), +(25195, 3, 18740, 1, 0, 51831); From 1ca428e8bb3c93a137b8c9568dee9352fd6b26b2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Jul 2026 15:37:17 +0000 Subject: [PATCH 36/47] chore(DB): import pending files Referenced commit(s): 4d5c23e1910ef04edbf538b11546f2032d18d799 --- .../rev_1783319870710449449.sql => db_world/2026_07_06_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783319870710449449.sql => db_world/2026_07_06_00.sql} (92%) diff --git a/data/sql/updates/pending_db_world/rev_1783319870710449449.sql b/data/sql/updates/db_world/2026_07_06_00.sql similarity index 92% rename from data/sql/updates/pending_db_world/rev_1783319870710449449.sql rename to data/sql/updates/db_world/2026_07_06_00.sql index 769c202a4..33bcc0bf0 100644 --- a/data/sql/updates/pending_db_world/rev_1783319870710449449.sql +++ b/data/sql/updates/db_world/2026_07_06_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_05_02 -> 2026_07_06_00 -- Move Marksman Bova to the correct position and model. UPDATE `creature` SET `position_x` = -1721.4673, `position_y` = 5637.9883, `position_z` = 128.10652, `orientation` = 2.321287870407104492 WHERE `guid` = 85406 AND `id` = 25195; From 5239460d1027bcc5a31ba887fdc07ecdc180fc09 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:34:54 -0300 Subject: [PATCH 37/47] fix(Scripts/Ulduar): spawn Mimiron chest via map and stop friendly reset (#26498) Co-authored-by: Claude Opus 4.8 --- .../scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 7 ++++++- 1 file changed, 6 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 87aace459..c0f979d77 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -758,10 +758,12 @@ struct boss_mimiron : public BossAI // spawn chest if (uint32 chestId = (_hardmode ? RAID_MODE(GO_MIMIRON_CHEST_HARD, GO_MIMIRON_CHEST_HERO_HARD) : RAID_MODE(GO_MIMIRON_CHEST, GO_MIMIRON_CHEST_HERO))) { - if (GameObject* go = me->SummonGameObject(chestId, 2744.65f, 2569.46f, 364.397f, 0, 0, 0, 0, 0, 0)) + // Summoned by the map, not Mimiron, so the chest survives his despawn during the outro. + if (GameObject* go = me->GetMap()->SummonGameObject(chestId, 2744.65f, 2569.46f, 364.397f, 0, 0, 0, 0, 0, 0)) { go->ReplaceAllGameObjectFlags((GameObjectFlags)0); go->SetLootRecipient(me->GetMap()); + go->SetRespawnTime(7 * DAY); } } events.ScheduleEvent(EVENT_DISAPPEAR, 9s); @@ -786,6 +788,9 @@ struct boss_mimiron : public BossAI void EnterEvadeMode(EvadeReason why) override { + // Once Mimiron turns friendly for the defeat RP, don't reset the encounter. + if (me->GetFaction() == FACTION_FRIENDLY) + return; if (_isEvading) return; _isEvading = true; From 0f107f2cd43aac8628a5f0163e28ee2858eab932 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:01:31 -0300 Subject: [PATCH 38/47] fix(Scripts/EoE): fix Malygos Phase 3 Surge of Power targeting (10-man) (#26515) Co-authored-by: Claude Fable 5 Co-authored-by: sogladev --- .../Nexus/EyeOfEternity/boss_malygos.cpp | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp index 38afc8509..ce575923c 100644 --- a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp +++ b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp @@ -679,25 +679,29 @@ struct boss_malygos : public BossAI { for (uint8 i = 0; i < NUM_MAX_SURGE_TARGETS; ++i) _surgeTargetGUID[i].Clear(); - me->CastSpell((Unit*)nullptr, SPELL_SURGE_OF_POWER_WARN_SELECTOR_25, true); + + DoCastAOE(SPELL_SURGE_OF_POWER_WARN_SELECTOR_25, true); me->m_Events.AddEventAtOffset([this] { - me->CastSpell((Unit*)nullptr, SPELL_PH3_SURGE_OF_POWER_25, true); + DoCastAOE(SPELL_PH3_SURGE_OF_POWER_25, true); }, 3s); } else { + for (uint8 i = 0; i < NUM_MAX_SURGE_TARGETS; ++i) + _surgeTargetGUID[i].Clear(); + if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 0.0f, false, true, SPELL_RIDE_RED_DRAGON_BUDDY)) { if (Vehicle* vehicle = target->GetVehicleKit()) if (Unit* passenger = vehicle->GetPassenger(0)) if (Player* player = passenger->ToPlayer()) Talk(EMOTE_SURGE_OF_POWER_WARNING_P3, player); - ObjectGuid targetGuid = target->GetGUID(); - me->m_Events.AddEventAtOffset([this, targetGuid] + + SetGUID(target->GetGUID(), DATA_FIRST_SURGE_TARGET_GUID); + me->m_Events.AddEventAtOffset([this] { - if (Unit* delayedTarget = ObjectAccessor::GetUnit(*me, targetGuid)) - me->CastSpell(delayedTarget, SPELL_PH3_SURGE_OF_POWER, true); + DoCastAOE(SPELL_PH3_SURGE_OF_POWER, true); }, 3s); } } @@ -1277,15 +1281,22 @@ class spell_eoe_ph3_surge_of_power : public SpellScript { PrepareSpellScript(spell_eoe_ph3_surge_of_power); + bool Load() override + { + return GetCaster()->IsCreature(); + } + void FilterTargets(std::list& targets) { - // Target selection and warning are handled in boss AI. - // Here we just restrict area targets to the explicit cast target. - if (Unit* explTarget = GetExplTargetUnit()) + // The spell targets an area, but only the drake that received the fixate warning + // should be hit. The boss AI stores that drake's GUID; keep only that target. + Creature* caster = GetCaster()->ToCreature(); + ObjectGuid targetGuid = caster->AI()->GetGUID(DATA_FIRST_SURGE_TARGET_GUID); + + targets.remove_if([targetGuid](WorldObject* target) { - targets.clear(); - targets.push_back(explTarget); - } + return target->GetGUID() != targetGuid; + }); } void Register() override From edb3730f7a6c61a9b413d1fd7697a1e1faeba3cf Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:34:14 -0300 Subject: [PATCH 39/47] fix(Core/Battlegrounds): don't block BG entry on a stale charm reference (#26487) Co-authored-by: Claude Opus 4.8 --- src/server/game/DungeonFinding/LFGMgr.cpp | 4 +++- src/server/game/Handlers/BattleGroundHandler.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index d3505b773..6651c7a01 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -2257,7 +2257,9 @@ namespace lfg { error = LFG_TELEPORTERROR_IN_VEHICLE; } - else if (player->GetCharmGUID() || player->IsInCombat()) + // GetCharm() validates the charmed unit still exists and clears a stale reference, + // unlike GetCharmGUID(); a despawned vehicle must not permanently block the teleport. + else if (player->GetCharm() || player->IsInCombat()) { error = LFG_TELEPORTERROR_COMBAT; } diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index 414be5d64..ff5a57c47 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -412,7 +412,9 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) return; } - if (_player->GetCharmGUID() || _player->IsInCombat()) + // GetCharm() validates the charmed unit still exists and clears a stale reference, + // unlike GetCharmGUID(); a despawned vehicle must not permanently block BG entry. + if (_player->GetCharm() || _player->IsInCombat()) { ChatHandler(_player->GetSession()).SendNotification(LANG_YOU_IN_COMBAT); return; From ac071f8f5d6bada3713abb1edcfbbc8e571e20d0 Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:36:22 -0400 Subject: [PATCH 40/47] fix(Core/Entities): Knock back players if they are not under client control. (#26540) Co-authored-by: Claude Co-authored-by: sogladev --- src/server/game/Entities/Unit/Unit.cpp | 8 +++++--- src/server/game/Handlers/MovementHandler.cpp | 12 +++++++++--- src/server/game/Movement/MotionMaster.cpp | 11 ++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 73219a924..90f2b2b03 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -15417,10 +15417,12 @@ void Unit::KnockbackFrom(float x, float y, float speedXY, float speedZ) } } - if (!player) - { + // While feared/confused the client has no control over the unit, + // so a SMSG_MOVE_KNOCK_BACK would be ignored or immediately overridden by the server + // side fleeing/confused splines. Perform the knockback server side instead; the + // fleeing/confused movement generator resumes once this spline is finalized + if (!player || !IsClientControlled()) GetMotionMaster()->MoveKnockbackFrom(x, y, speedXY, speedZ); - } else { float vcos, vsin; diff --git a/src/server/game/Handlers/MovementHandler.cpp b/src/server/game/Handlers/MovementHandler.cpp index 3bca8bbfc..46140f5a4 100644 --- a/src/server/game/Handlers/MovementHandler.cpp +++ b/src/server/game/Handlers/MovementHandler.cpp @@ -840,14 +840,20 @@ void WorldSession::HandleMoveKnockBackAck(WorldPacket& recvData) movementInfo.guid = guid; ReadMovementInfo(recvData, &movementInfo); - mover->m_movementInfo = movementInfo; + // Relocate the mover to the acknowledged position. Otherwise the server (and the + // MSG_MOVE_KNOCK_BACK broadcast below) keeps using the pre-knockback position until + // the next regular movement packet arrives, desyncing the unit for nearby clients + if (!ProcessMovementInfo(movementInfo, mover, mover->ToPlayer(), recvData)) + { + recvData.rfinish(); // prevent warnings spam + return; + } if (mover->IsPlayer() && static_cast(mover)->IsFreeFlying()) mover->SetCanFly(true); WorldPacket data(MSG_MOVE_KNOCK_BACK, 66); - data << guid.WriteAsPacked(); - _player->m_mover->BuildMovementPacket(&data); + WriteMovementInfo(&data, &movementInfo); _player->SetCanTeleport(true); // knockback specific info data << movementInfo.jump.sinAngle; diff --git a/src/server/game/Movement/MotionMaster.cpp b/src/server/game/Movement/MotionMaster.cpp index aba6e2c7b..96b7f470f 100644 --- a/src/server/game/Movement/MotionMaster.cpp +++ b/src/server/game/Movement/MotionMaster.cpp @@ -620,7 +620,7 @@ void MotionMaster::MoveTakeoff(uint32 id, float x, float y, float z, float speed void MotionMaster::MoveKnockbackFrom(float srcX, float srcY, float speedXY, float speedZ) { //this function may make players fall below map - if (_owner->IsPlayer()) + if (_owner->IsPlayer() && _owner->IsClientControlled()) return; if (speedXY <= 0.1f) @@ -640,6 +640,15 @@ void MotionMaster::MoveKnockbackFrom(float srcX, float srcY, float speedXY, floa init.SetOrientationFixed(true); init.SetVelocity(speedXY); + // Do not mutate an active fleeing/confused movement generator, + // doing so breaks the movement upon landing from the knockback + MovementGeneratorType slotType = GetMotionSlotType(MOTION_SLOT_CONTROLLED); + if (slotType == FLEEING_MOTION_TYPE || slotType == CONFUSED_MOTION_TYPE) + { + init.Launch(); + return; + } + Mutate(new EffectMovementGenerator(init, 0), MOTION_SLOT_CONTROLLED); } From 4472155a6d6aa31ec7b7cd654f716ebccf12452f Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:41:00 -0300 Subject: [PATCH 41/47] fix(Core/Unit): start pet combat on contact instead of on attack command (#26463) Co-authored-by: Claude Fable 5 Co-authored-by: sogladev --- src/server/game/Entities/Unit/Unit.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 90f2b2b03..535417af1 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -7425,22 +7425,21 @@ bool Unit::Attack(Unit* victim, bool meleeAttack) // set position before any AI calls/assistance //if (IsCreature()) // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); - if (creature) + // player-controlled creatures (pets, charms) enter combat on contact instead + // (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked) + if (creature && !IsControlledByPlayer()) { EngageWithTarget(victim); - if (!IsControlledByPlayer()) - { - creature->SendAIReaction(AI_REACTION_HOSTILE); + 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 From cfee83d19f134afce6d1f25bbd0e81fd6f11db4d Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:41:33 -0300 Subject: [PATCH 42/47] fix(Scripts/Mage): stop Mirror Image from breaking crowd control (#26458) Co-authored-by: Claude Opus 4.8 --- src/server/scripts/Pet/pet_mage.cpp | 41 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/server/scripts/Pet/pet_mage.cpp b/src/server/scripts/Pet/pet_mage.cpp index 6ccf7673f..e3852eeb3 100644 --- a/src/server/scripts/Pet/pet_mage.cpp +++ b/src/server/scripts/Pet/pet_mage.cpp @@ -36,7 +36,9 @@ enum MageSpells SPELL_SUMMON_MIRROR_IMAGE1 = 58831, SPELL_SUMMON_MIRROR_IMAGE2 = 58833, SPELL_SUMMON_MIRROR_IMAGE3 = 58834, - SPELL_SUMMON_MIRROR_IMAGE_GLYPH = 65047 + SPELL_SUMMON_MIRROR_IMAGE_GLYPH = 65047, + SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT = 59638, + SPELL_MAGE_MIRROR_IMAGE_FIRE_BLAST = 59637 }; class DeathEvent : public BasicEvent @@ -202,24 +204,45 @@ struct npc_pet_mage_mirror_image : CasterAI return; } - checktarget += diff; + // A dead target, or one we lost sight of, is invalid: drop it and reselect. + // CanSeeOrDetect is comparatively expensive, so throttle the sight check to ~1s. + bool lostTarget = !me->GetVictim()->IsAlive(); + checktarget += diff; if (checktarget >= 1000) { - if (!me->GetVictim()->IsAlive() || me->GetVictim()->HasBreakableByDamageCrowdControlAura() || !me->CanSeeOrDetect(me->GetVictim())) - { - MySelectNextTarget(); - me->InterruptNonMeleeSpells(true); - return; - } + checktarget = 0; + if (!me->CanSeeOrDetect(me->GetVictim())) + lostTarget = true; } + if (lostTarget) + { + MySelectNextTarget(); + me->InterruptNonMeleeSpells(false); + return; + } + + // A cast already in progress when the crowd control lands is allowed to finish (3.1.2), + // except a Frostbolt on a target that is now Polymorphed: 3.2.0 cancels that cast so it + // cannot break the Polymorph. Other breakable CC keeps the "let it finish" behaviour. if (me->HasUnitState(UNIT_STATE_CASTING)) + { + if (me->GetVictim()->HasAuraWithMechanic(1ULL << MECHANIC_POLYMORPH) + && me->FindCurrentSpellBySpellId(SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT)) + me->InterruptNonMeleeSpells(false, SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT); + + return; + } + + // Never start a new cast on a target under a breakable-by-damage CC aura (Polymorph, + // Dragon's Breath, ...) - that is what would break the crowd control. + if (me->GetVictim()->HasBreakableByDamageCrowdControlAura(me)) return; if (uint32 spellId = events.ExecuteEvent()) { - events.RescheduleEvent(spellId, spellId == 59637 ? 6500ms : 2500ms); + events.RescheduleEvent(spellId, spellId == SPELL_MAGE_MIRROR_IMAGE_FIRE_BLAST ? 6500ms : 2500ms); me->CastSpell(me->GetVictim(), spellId, false); } } From 497cd60dcf9c5d6b0140ac91235f0923f8dbad64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=90=E5=A6=82=E4=BB=99?= <18535853+PkllonG@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:28:55 +0800 Subject: [PATCH 43/47] =?UTF-8?q?fix(Core/SmartAI):=20Fix=20SAI's=20SetFol?= =?UTF-8?q?low=20behavior=20so=20it=20doesn=E2=80=99t=20inherit=20movement?= =?UTF-8?q?=20speed=20(#26539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/game/AI/SmartScripts/SmartAI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index eec86a422..23d7466f5 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -1239,7 +1239,7 @@ void SmartAI::SetFollow(Unit* target, float dist, float angle, uint32 credit, ui mFollowArrivedEntry = end; mFollowArrivedAlive = !aliveState; // negate - 0 is alive mFollowCreditType = creditType; - me->GetMotionMaster()->MoveFollow(target, mFollowDist, mFollowAngle); + me->GetMotionMaster()->MoveFollow(target, mFollowDist, mFollowAngle, MOTION_SLOT_ACTIVE, true, false); } void SmartAI::StopFollow(bool complete) From f6974e78208a1f2013a6fb2f412db05a3ecd9268 Mon Sep 17 00:00:00 2001 From: sogladev Date: Sat, 11 Jul 2026 11:58:43 +0200 Subject: [PATCH 44/47] chore(SmartAI): add missing doc `SMART_ACTION_FOLLOW` (#26547) --- src/server/game/AI/SmartScripts/SmartScriptMgr.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/AI/SmartScripts/SmartScriptMgr.h b/src/server/game/AI/SmartScripts/SmartScriptMgr.h index a8ae1cc9f..04804584c 100644 --- a/src/server/game/AI/SmartScripts/SmartScriptMgr.h +++ b/src/server/game/AI/SmartScripts/SmartScriptMgr.h @@ -568,7 +568,7 @@ enum SMART_ACTION SMART_ACTION_CALL_GROUPEVENTHAPPENS = 26, // QuestID SMART_ACTION_COMBAT_STOP = 27, // No Params SMART_ACTION_REMOVEAURASFROMSPELL = 28, // Spellid (0 removes all auras), charges (0 removes aura) - SMART_ACTION_FOLLOW = 29, // Distance (0 = default), Angle (0 = default), EndCreatureEntry, credit, creditType (0monsterkill, 1event) + SMART_ACTION_FOLLOW = 29, // Distance (0 = default), Angle (0 = default), EndCreatureEntry, credit, creditType (0monsterkill, 1event), aliveState (0 = creature must be alive, 1 = dead creature can trigger arrival) SMART_ACTION_RANDOM_PHASE = 30, // PhaseId1, PhaseId2, PhaseId3... SMART_ACTION_RANDOM_PHASE_RANGE = 31, // PhaseMin, PhaseMax SMART_ACTION_RESET_GOBJECT = 32, // From 412d143bf822efd2f8cdeef88f6281e8aef9818e Mon Sep 17 00:00:00 2001 From: Vox Date: Sat, 11 Jul 2026 13:50:11 -0400 Subject: [PATCH 45/47] fix(db/Creature): Wrymrest Temple NPC Improvements (#26512) Co-authored-by: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> --- .../rev_1783370489285413800.sql | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1783370489285413800.sql diff --git a/data/sql/updates/pending_db_world/rev_1783370489285413800.sql b/data/sql/updates/pending_db_world/rev_1783370489285413800.sql new file mode 100644 index 000000000..0148fdd89 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783370489285413800.sql @@ -0,0 +1,101 @@ +-- --------------------------------------------------------------------------- +-- Warden of the Chamber (30058) +-- --------------------------------------------------------------------------- +-- populate warden transform spells +-- Effect_1 = 6 (SPELL_EFFECT_APPLY_AURA), EffectAura_1 = 56 +-- 55831 Red - 30072 (display 26747) +-- 55830 Bronze - 30059 (display 26741) +-- 55828 Blue - 30076 (display 26749) +-- 55829 Green - 30073 (display 26748) +-- 55827 Black - 30077 (display 14308) +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30072 WHERE `ID` = 55831; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30059 WHERE `ID` = 55830; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30076 WHERE `ID` = 55828; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30073 WHERE `ID` = 55829; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30077 WHERE `ID` = 55827; + +-- creature_addon apply each warden transform as a passive spawn aura +-- per-GUID flight assignment +-- Blue 105487, 105495 - 55828 +-- Bronze 105488, 105489 - 55830 +-- Green 131055, 131058 - 55829 +-- Red 131056, 131059 - 55831 + 29266 (Permanent Feign Death, 3.3.5a accurate) +-- Black 131063, 131064 - 55827 +DELETE FROM `creature_addon` WHERE `guid` IN (105487, 105488, 105489, 105495, 131055, 131056, 131058, 131059, 131063, 131064); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(105487, 0, 0, 0, 1, 0, 0, '55828'), +(105488, 0, 0, 0, 1, 0, 0, '55830'), +(105489, 0, 0, 0, 1, 0, 0, '55830'), +(105495, 0, 0, 0, 1, 0, 0, '55828'), +(131055, 0, 0, 0, 1, 0, 0, '55829'), +(131056, 0, 0, 0, 1, 0, 0, '55831 29266'), +(131058, 0, 0, 0, 1, 0, 0, '55829'), +(131059, 0, 0, 0, 1, 0, 0, '55831 29266'), +(131063, 0, 0, 0, 1, 0, 0, '55827'), +(131064, 0, 0, 0, 1, 0, 0, '55827'); + +-- Red fire wall that blocks Ruby Sanctum (Invisible Stalker (23155) GUID 131066) stop spawn (intentional) +UPDATE `creature` SET `spawnMask` = 0 WHERE `guid` = 131066 AND `id` = 23155; + +-- Correct blue warden 105487 orientation +UPDATE `creature` SET `orientation` = 2.990109920501708984 WHERE `guid` = 105487 AND `id` = 30058; + +-- --------------------------------------------------------------------------- +-- Wyrmrest Protector (27953) +-- --------------------------------------------------------------------------- +-- populate protector transform spells +-- 50158 Red - 27952 (display 14357) +-- 51118 Blue - 28251 (display 14356) +-- 50160 Bronze - 27955 (display 14358) +-- 50159 Green - 27954 (display 14359) +-- 51117 Black - 28250 (display 14355) +-- 51119 Nether - 28252 (display 25257) +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27952 WHERE `ID` = 50158; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28251 WHERE `ID` = 51118; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27955 WHERE `ID` = 50160; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27954 WHERE `ID` = 50159; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28250 WHERE `ID` = 51117; +UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28252 WHERE `ID` = 51119; + +-- --------------------------------------------------------------------------- +-- SAI on respawn, roll 1 of 6 spells and apply spell transform +-- matching polearm is equipped depending what spell is selected +-- 2795301 Red - aura 50158, polearm 38488 +-- 2795302 Blue - aura 51118, polearm 32729 +-- 2795303 Bronze - aura 50160, polearm 38491 +-- 2795304 Green - aura 50159, polearm 38209 +-- 2795305 Black - aura 51117, polearm 38487 +-- 2795306 Nether - aura 51119, polearm 38490 +DELETE FROM `smart_scripts` WHERE `entryorguid` = 27953 AND `source_type` = 0 AND `id` = 4; +DELETE FROM `smart_scripts` WHERE `entryorguid` IN (2795301, 2795302, 2795303, 2795304, 2795305, 2795306) 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 +(27953, 0, 4, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 87, 2795301, 2795302, 2795303, 2795304, 2795305, 2795306, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector - On Respawn - Random flight transform + polearm'), +(2795301, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50158, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Red) - Transform aura'), +(2795301, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38488, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Red) - Equip polearm'), +(2795302, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51118, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Blue) - Transform aura'), +(2795302, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 32729, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Blue) - Equip polearm'), +(2795303, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50160, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Bronze) - Transform aura'), +(2795303, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38491, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Bronze) - Equip polearm'), +(2795304, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50159, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Green) - Transform aura'), +(2795304, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38209, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Green) - Equip polearm'), +(2795305, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51117, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Black) - Transform aura'), +(2795305, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38487, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Black) - Equip polearm'), +(2795306, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51119, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Nether) - Transform aura'), +(2795306, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38490, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Nether) - Equip polearm'); + +-- 10 GUIDs are given emote state 173 +DELETE FROM `creature_addon` WHERE `guid` IN (131012, 131014, 131015, 131016, 131020, 131021, 131022, 131023, 131026, 131027); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(131012, 0, 0, 0, 1, 173, 0, NULL), +(131014, 0, 0, 0, 1, 173, 0, NULL), +(131015, 0, 0, 0, 1, 173, 0, NULL), +(131016, 0, 0, 0, 1, 173, 0, NULL), +(131020, 0, 0, 0, 1, 173, 0, NULL), +(131021, 0, 0, 0, 1, 173, 0, NULL), +(131022, 0, 0, 0, 1, 173, 0, NULL), +(131023, 0, 0, 0, 1, 173, 0, NULL), +(131026, 0, 0, 0, 1, 173, 0, NULL), +(131027, 0, 0, 0, 1, 173, 0, NULL); + +-- Reposition Protector spawn 131030 to its sniffed location +UPDATE `creature` SET `position_x` = 3452.6472, `position_y` = 250.009, `position_z` = 52.378803, `orientation` = 3.298672199249267578 WHERE `guid` = 131030 AND `id` = 27953; From 8e48af8091a09b59c6e525e95e1d9008e8f110d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Jul 2026 17:51:25 +0000 Subject: [PATCH 46/47] chore(DB): import pending files Referenced commit(s): 412d143bf822efd2f8cdeef88f6281e8aef9818e --- .../rev_1783370489285413800.sql => db_world/2026_07_11_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783370489285413800.sql => db_world/2026_07_11_00.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1783370489285413800.sql b/data/sql/updates/db_world/2026_07_11_00.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1783370489285413800.sql rename to data/sql/updates/db_world/2026_07_11_00.sql index 0148fdd89..0ecf9828d 100644 --- a/data/sql/updates/pending_db_world/rev_1783370489285413800.sql +++ b/data/sql/updates/db_world/2026_07_11_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_06_00 -> 2026_07_11_00 -- --------------------------------------------------------------------------- -- Warden of the Chamber (30058) -- --------------------------------------------------------------------------- From 82f87a8b82d38833790227444261e2a0c6dbbbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Borz=C3=AC?= Date: Sat, 11 Jul 2026 22:32:41 +0200 Subject: [PATCH 47/47] fix(Core/Battlegrounds): fix invited-count accounting on BG entry (#26556) --- .../game/Battlegrounds/BattlegroundMgr.cpp | 11 ++++---- .../game/Battlegrounds/BattlegroundMgr.h | 5 +++- .../game/Battlegrounds/BattlegroundQueue.cpp | 9 ++++--- .../game/Handlers/BattleGroundHandler.cpp | 26 ++++++++++++++++++- src/server/scripts/Commands/cs_misc.cpp | 15 ++++++++++- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp index 3c2411f7f..c03323e4b 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp +++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp @@ -647,7 +647,7 @@ void BattlegroundMgr::BuildBattlegroundListPacket(WorldPacket* data, ObjectGuid } } -void BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, BattlegroundTypeId bgTypeId) +bool BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, BattlegroundTypeId bgTypeId) { if (Battleground* bg = GetBattleground(instanceId, bgTypeId)) { @@ -655,12 +655,11 @@ void BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, Batt Position const* pos = bg->GetTeamStartPosition(player->GetBgTeamId()); LOG_DEBUG("bg.battleground", "BattlegroundMgr::SendToBattleground: Sending {} to map {}, {} (bgType {})", player->GetName(), mapid, pos->ToString(), bgTypeId); - player->TeleportTo(mapid, pos->GetPositionX(), pos->GetPositionY(), pos->GetPositionZ(), pos->GetOrientation()); - } - else - { - LOG_ERROR("bg.battleground", "BattlegroundMgr::SendToBattleground: Instance {} (bgType {}) not found while trying to teleport player {}", instanceId, bgTypeId, player->GetName()); + return player->TeleportTo(mapid, pos->GetPositionX(), pos->GetPositionY(), pos->GetPositionZ(), pos->GetOrientation()); } + + LOG_ERROR("bg.battleground", "BattlegroundMgr::SendToBattleground: Instance {} (bgType {}) not found while trying to teleport player {}", instanceId, bgTypeId, player->GetName()); + return false; } void BattlegroundMgr::SendAreaSpiritHealerQueryOpcode(Player* player, Battleground* bg, ObjectGuid guid) diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.h b/src/server/game/Battlegrounds/BattlegroundMgr.h index 313bee8f0..50d5cc27a 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.h +++ b/src/server/game/Battlegrounds/BattlegroundMgr.h @@ -93,7 +93,10 @@ public: void LoadBattlegroundTemplates(); void DeleteAllBattlegrounds(); - void SendToBattleground(Player* player, uint32 InstanceID, BattlegroundTypeId bgTypeId); + // Returns false when the teleport could not start (instance gone, or a + // synchronous TeleportTo failure) so the accept path can release the + // otherwise-orphaned invited reservation. + bool SendToBattleground(Player* player, uint32 InstanceID, BattlegroundTypeId bgTypeId); /* Battleground queues */ BattlegroundQueue& GetBattlegroundQueue(BattlegroundQueueTypeId bgQueueTypeId) { return m_BattlegroundQueues[bgQueueTypeId]; } diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.cpp b/src/server/game/Battlegrounds/BattlegroundQueue.cpp index 169f642ec..1fac5ad54 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.cpp +++ b/src/server/game/Battlegrounds/BattlegroundQueue.cpp @@ -1302,13 +1302,16 @@ int32 BattlegroundQueue::GetQueueAnnouncementTimer(uint32 bracketId) const void BattlegroundQueue::InviteGroupToBG(GroupQueueInfo* ginfo, Battleground* bg, TeamId teamId) { + // An already-invited group keeps the side it was invited under: writing + // teamId here would split a future re-invite's IncreaseInvitedCount from the + // original side's DecreaseInvitedCount at leave, desyncing the ledger. + if (ginfo->IsInvitedToBGInstanceGUID) + return; + // set side if needed if (teamId != TEAM_NEUTRAL) ginfo->teamId = teamId; - if (ginfo->IsInvitedToBGInstanceGUID) - return; - // set invitation ginfo->IsInvitedToBGInstanceGUID = bg->GetInstanceID(); diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index ff5a57c47..9051801e5 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -544,7 +544,31 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) sLFGMgr->LeaveAllLfgQueues(_player->GetGUID(), false); _player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), queueSlot, true, bgTypeId == BATTLEGROUND_RB, teamId); - sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId); + + if (!sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId)) + { + // The teleport never started (instance gone, or a synchronous + // TeleportTo veto such as a DK still locked to Ebon Hold). The accept + // already pulled the player out of the queue and he can't decline + // now, so undo the accept here -- otherwise the invited reservation + // leaks forever, permanently skewing team selection and blocking the + // empty instance's cleanup. + bg->DecreaseInvitedCount(teamId); + _player->RemoveBattlegroundQueueId(bgQueueTypeId); + _player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL); + + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); + SendPacket(&data); + + // Free slot -> let the queue refill it. BG only, like the sibling + // leave-queue path: an arena update needs its own type/rating. + if (!ginfo.ArenaType) + sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId()); + + LOG_ERROR("bg.battleground", "Battleground: player {} {} failed to teleport into bg {}, bgtype {}; released the invited reservation.", + _player->GetName(), _player->GetGUID().ToString(), bg->GetInstanceID(), bg->GetBgTypeID()); + return; + } LOG_DEBUG("bg.battleground", "Battleground: player {} {} joined battle for bg {}, bgtype {}, queue type {}.", _player->GetName(), _player->GetGUID().ToString(), bg->GetInstanceID(), bg->GetBgTypeID(), bgQueueTypeId); } diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index 6b66f6f27..456628d03 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -499,8 +499,21 @@ public: // Remove from LFG queues sLFGMgr->LeaveAllLfgQueues(player->GetGUID(), false); + // Book the reservation like the queue path does, so it is symmetric + // with RemovePlayerAtLeave's decrement and the 0-players/0-invited + // state can't let Battleground::Update delete the arena while players + // are still on the loading screen. + bg->IncreaseInvitedCount(teamId); player->SetBattlegroundId(bg->GetInstanceID(), bgTypeId, queueSlot, true, false, teamId); - sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId); + + // A synchronous teleport failure would strand that reservation (the + // player never enters and never reaches RemovePlayerAtLeave), leaving + // the arena undeletable; release it and reset his bg data. + if (!sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId)) + { + bg->DecreaseInvitedCount(teamId); + player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL); + } } handler->PSendSysMessage("Success! Players are now being teleported to the arena.");