From 9bf81ddc84fab7fa71fdbcdd4c88e4980acc531c Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:09:27 -0300 Subject: [PATCH 01/69] fix(Scripts/Dragonblight): Group Credit for The Chain Gun and You (#26785) --- src/server/scripts/Northrend/zone_dragonblight.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/zone_dragonblight.cpp b/src/server/scripts/Northrend/zone_dragonblight.cpp index 9e2781dba..34546029e 100644 --- a/src/server/scripts/Northrend/zone_dragonblight.cpp +++ b/src/server/scripts/Northrend/zone_dragonblight.cpp @@ -630,7 +630,7 @@ public: if (TempSummon* summon = me->ToTempSummon()) if (Unit* owner = summon->GetSummonerUnit()) if (Player* player = owner->ToPlayer()) - player->KilledMonsterCredit(me->GetEntry()); + player->RewardPlayerAndGroupAtEvent(me->GetEntry(), player); } } }; From a8be89f12150735b2baed1b978b09a0341a9ec13 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:48:20 -0300 Subject: [PATCH 02/69] fix(DB/SAI): Group Completion for A Righteous Sermon (12321) (#26784) --- data/sql/updates/pending_db_world/rev_1784942926675029900.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784942926675029900.sql diff --git a/data/sql/updates/pending_db_world/rev_1784942926675029900.sql b/data/sql/updates/pending_db_world/rev_1784942926675029900.sql new file mode 100644 index 000000000..ee2c81507 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784942926675029900.sql @@ -0,0 +1,2 @@ +-- +UPDATE `smart_scripts` SET `action_type` = 26, `target_type` = 16 WHERE (`entryorguid` = 2731600) AND (`source_type` = 9) AND (`id` IN (34)); From f5db7d8d7230c14160e909598c8e6846110e3fbd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 07:49:33 +0000 Subject: [PATCH 03/69] chore(DB): import pending files Referenced commit(s): a8be89f12150735b2baed1b978b09a0341a9ec13 --- .../rev_1784942926675029900.sql => db_world/2026_07_25_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784942926675029900.sql => db_world/2026_07_25_00.sql} (76%) diff --git a/data/sql/updates/pending_db_world/rev_1784942926675029900.sql b/data/sql/updates/db_world/2026_07_25_00.sql similarity index 76% rename from data/sql/updates/pending_db_world/rev_1784942926675029900.sql rename to data/sql/updates/db_world/2026_07_25_00.sql index ee2c81507..d830381f4 100644 --- a/data/sql/updates/pending_db_world/rev_1784942926675029900.sql +++ b/data/sql/updates/db_world/2026_07_25_00.sql @@ -1,2 +1,3 @@ +-- DB update 2026_07_24_05 -> 2026_07_25_00 -- UPDATE `smart_scripts` SET `action_type` = 26, `target_type` = 16 WHERE (`entryorguid` = 2731600) AND (`source_type` = 9) AND (`id` IN (34)); From da2047ea03bde36fb41f8b36bbee4ec8c96a9236 Mon Sep 17 00:00:00 2001 From: Saqra1 <161769195+Saqra1@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:16:34 -0500 Subject: [PATCH 04/69] fix(Core/Achievements): Track World Wide Winner arena wins (#26772) --- src/server/game/Achievements/AchievementMgr.cpp | 2 +- src/server/game/Battlegrounds/Arena.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/game/Achievements/AchievementMgr.cpp b/src/server/game/Achievements/AchievementMgr.cpp index d538a3863..55cb41b42 100644 --- a/src/server/game/Achievements/AchievementMgr.cpp +++ b/src/server/game/Achievements/AchievementMgr.cpp @@ -1775,7 +1775,7 @@ void AchievementMgr::UpdateAchievementCriteria(AchievementCriteriaTypes type, ui { // those requirements couldn't be found in the dbc AchievementCriteriaDataSet const* data = sAchievementMgr->GetCriteriaDataSet(achievementCriteria); - if (!data || !data->Meets(GetPlayer(), nullptr)) + if (!data || !data->Meets(GetPlayer(), unit)) continue; // Check map id requirement diff --git a/src/server/game/Battlegrounds/Arena.cpp b/src/server/game/Battlegrounds/Arena.cpp index 2f8b0e54e..fffa1b062 100644 --- a/src/server/game/Battlegrounds/Arena.cpp +++ b/src/server/game/Battlegrounds/Arena.cpp @@ -342,7 +342,7 @@ void Arena::EndBattleground(TeamId winnerTeamId) // update achievement BEFORE personal rating update uint32 rating = player->GetArenaPersonalRating(winnerArenaTeam->GetSlot()); player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_WIN_RATED_ARENA, rating ? rating : 1); - player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_WIN_ARENA, GetMapId()); + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_WIN_ARENA, GetMapId(), 0, player); // Last standing - Rated 5v5 arena & be solely alive player if (GetArenaType() == ARENA_TYPE_5v5 && aliveWinners == 1 && player->IsAlive()) From 3117e4a4ff6de2897ef396c762b65df98f7acaa0 Mon Sep 17 00:00:00 2001 From: CarloHagens Date: Sat, 25 Jul 2026 14:21:18 +0200 Subject: [PATCH 05/69] fix(Core/Instances): day-align expired reset time (#26711) Co-authored-by: Claude Fable 5 --- src/server/game/Instances/InstanceSaveMgr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/game/Instances/InstanceSaveMgr.cpp b/src/server/game/Instances/InstanceSaveMgr.cpp index da03677fe..76e0d2db0 100644 --- a/src/server/game/Instances/InstanceSaveMgr.cpp +++ b/src/server/game/Instances/InstanceSaveMgr.cpp @@ -354,7 +354,7 @@ void InstanceSaveMgr::LoadResetTimes() { // assume that expired instances have already been cleaned // calculate the next reset time - t = (t * DAY) / DAY; + t = (t / DAY) * DAY; t += ((today - t) / period + 1) * period + diff; CharacterDatabase.DirectExecute("UPDATE instance_reset SET resettime = '{}' WHERE mapid = '{}' AND difficulty = '{}'", (uint32)t, mapid, difficulty); } From 0b871ee52a17da1eb71d614b834a81b09ee34c3d Mon Sep 17 00:00:00 2001 From: Vox Date: Sat, 25 Jul 2026 10:10:56 -0400 Subject: [PATCH 06/69] fix(Core/Spells): remove duplicate Decahedral Dwarven Dice emote hardcode (#26765) --- src/server/game/Spells/SpellEffects.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 0c57476a5..207bdd554 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -3932,17 +3932,6 @@ void Spell::EffectScriptEffect(SpellEffIndex effIndex) } break; }*/ - // Roll Dice - Decahedral Dwarven Dice - case 47770: - { - char buf[128]; - char const* gender = "his"; - if (m_caster->getGender() > 0) - gender = "her"; - snprintf(buf, sizeof(buf), "%s rubs %s [Decahedral Dwarven Dice] between %s hands and rolls. One %u and one %u.", m_caster->GetName().c_str(), gender, gender, urand(1, 10), urand(1, 10)); - m_caster->TextEmote(buf); - break; - } case 52173: // Coyote Spirit Despawn case 60243: // Blood Parrot Despawn if (unitTarget->IsCreature() && unitTarget->ToCreature()->IsSummon()) From f4a1b89a588fac903967695120f26a94b68c519c Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:19:29 -0300 Subject: [PATCH 07/69] fix(Scripts/TempleOfAhnQiraj): pass aggro target to Skeram's aggro yell (#26789) --- .../scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp index 69adba859..4661185b3 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp @@ -142,7 +142,7 @@ struct boss_skeram : public BossAI me->RemoveCorpse(); } - void JustEngagedWith(Unit* /*who*/) override + void JustEngagedWith(Unit* who) override { _JustEngagedWith(); events.Reset(); @@ -154,7 +154,9 @@ struct boss_skeram : public BossAI if (!me->IsSummon()) { - Talk(SAY_AGGRO); + // Resolve pets/guardians to their owner so gendered locales resolve $g against the puller + Player* puller = who->GetCharmerOrOwnerPlayerOrPlayerItself(); + Talk(SAY_AGGRO, puller ? puller : who); } } From 26b89edac5a22f31adce685b10ea66f087bc8f64 Mon Sep 17 00:00:00 2001 From: Skjalf <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:44:17 -0300 Subject: [PATCH 08/69] fix(Scripts/TempleOfAhnQiraj): use Unit* for puller to fix build --- src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp index 4661185b3..4b9fe0b49 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp @@ -155,7 +155,7 @@ struct boss_skeram : public BossAI if (!me->IsSummon()) { // Resolve pets/guardians to their owner so gendered locales resolve $g against the puller - Player* puller = who->GetCharmerOrOwnerPlayerOrPlayerItself(); + Unit* puller = who->GetCharmerOrOwnerPlayerOrPlayerItself(); Talk(SAY_AGGRO, puller ? puller : who); } } From f7eae55ebdca5d13637d84951cea205019992244 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:02:34 -0300 Subject: [PATCH 09/69] fix(Scripts/TempleOfAhnQiraj): fix GCC/Clang build error in Skeram aggro yell (#26802) --- src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp index 4661185b3..f47580b5e 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_skeram.cpp @@ -16,6 +16,7 @@ */ #include "CreatureScript.h" +#include "Player.h" #include "ScriptedCreature.h" #include "SpellScript.h" #include "SpellScriptLoader.h" @@ -155,7 +156,7 @@ struct boss_skeram : public BossAI if (!me->IsSummon()) { // Resolve pets/guardians to their owner so gendered locales resolve $g against the puller - Player* puller = who->GetCharmerOrOwnerPlayerOrPlayerItself(); + Unit* puller = who->GetCharmerOrOwnerPlayerOrPlayerItself(); Talk(SAY_AGGRO, puller ? puller : who); } } From 7069d99dca92ef21eaf9cb34a72a825470a78414 Mon Sep 17 00:00:00 2001 From: daobashun <49193927+fangshun2004@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:47:25 +0800 Subject: [PATCH 10/69] fix(Scripts/ScarletMonastery): Scarlet Monastery Forgiveness Spell Visual (#26454) --- .../rev_1783128746972420200.sql | 13 ++++ .../instance_scarlet_monastery.cpp | 71 +++++++++++++++---- 2 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1783128746972420200.sql diff --git a/data/sql/updates/pending_db_world/rev_1783128746972420200.sql b/data/sql/updates/pending_db_world/rev_1783128746972420200.sql new file mode 100644 index 000000000..bc5e22aa6 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1783128746972420200.sql @@ -0,0 +1,13 @@ +-- SPELL_TRANSFORM_GHOST and SPELL_FORGIVENESS + +DELETE FROM `spell_script_names` WHERE `spell_id` IN (28443, 28697); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(28443, 'spell_transform_ghost_visual'), +(28697, 'spell_forgiveness_dummy_visual'); + +-- Smartscript +-- Deleted 28 due to SPELL_FORGIVENESS attack death +DELETE FROM `smart_scripts` WHERE (`entryorguid` = 3976) AND (`source_type` = 0) AND (`id` IN (24, 28, 29)); +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 +(3976, 0, 24, 28, 61, 0, 100, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 'Scarlet Commander Mograine - On Reached Point 1 - Start Attacking'),-- Change link 29 to 28 +(3976, 0, 28, 0, 61, 0, 100, 0, 0, 0, 0, 0, 0, 0, 117, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Scarlet Commander Mograine - On Reached Point 1 - Enable Evade');-- Change id 29 to 28 diff --git a/src/server/scripts/EasternKingdoms/ScarletMonastery/instance_scarlet_monastery.cpp b/src/server/scripts/EasternKingdoms/ScarletMonastery/instance_scarlet_monastery.cpp index 04e2e1337..ba72ead37 100644 --- a/src/server/scripts/EasternKingdoms/ScarletMonastery/instance_scarlet_monastery.cpp +++ b/src/server/scripts/EasternKingdoms/ScarletMonastery/instance_scarlet_monastery.cpp @@ -18,6 +18,8 @@ #include "InstanceMapScript.h" #include "scarletmonastery.h" #include "ScriptedCreature.h" +#include "SpellScript.h" +#include "SpellScriptLoader.h" enum AshbringerEventMisc { @@ -39,17 +41,6 @@ enum AshbringerEventMisc GO_HIGH_INQUISITOR_DOOR = 104600 }; -enum AshbringerSpell -{ - //Highlord Mograine Spells - //Needs Fix: Increased the visual effect of spells on hit - SPELL_FORGIVENESS = 28697, - - //High Inquisitor Fairbanks - //Needs Fix: Increased the visual effect of spells on hit - SPELL_TRANSFORM_GHOST = 28443 -}; - enum DataTypes { TYPE_MOGRAINE_AND_WHITE_EVENT = 1, @@ -66,7 +57,7 @@ enum DataTypes GAMEOBJECT_PUMPKIN_SHRINE = 10 }; -float const CATHEDRAL_PULL_RANGE = 80.0f; // Distance from the Cathedral doors to where Mograine is standing +float constexpr CATHEDRAL_PULL_RANGE = 80.0f; // Distance from the Cathedral doors to where Mograine is standing class instance_scarlet_monastery : public InstanceMapScript { @@ -271,7 +262,63 @@ public: }; }; +enum AshbringerSpell +{ + SPELL_FORGIVENESS = 28697, + SPELL_FORGIVENESS_IMPACTKIT = 317, + SPELL_TRANSFORM_GHOST = 28443, + SPELL_TRANSFORM_IMPACTKIT=500 +}; + +// SPELL_FORGIVENESS = 28697 +class spell_forgiveness_dummy_visual : public SpellScript +{ + PrepareSpellScript(spell_forgiveness_dummy_visual); + + void HandleDummy(SpellEffIndex /*effIndex*/) + { + Unit* target = GetHitUnit(); + if (!target) + return; + + target->SendPlaySpellVisual(SPELL_FORGIVENESS_IMPACTKIT);//SPELL_FORGIVENESS IMPACTKIT 317 SpellVisualEntry.ImpactKit can't be used + + //Delay death to prevent the death of the creature from interrupting the animation display + target->m_Events.AddEventAtOffset([target]() -> void + { + target->KillSelf(); + }, 500ms); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_forgiveness_dummy_visual::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); + } +}; + +// SPELL_TRANSFORM_GHOST = 28443 +class spell_transform_ghost_visual: public SpellScript +{ + PrepareSpellScript(spell_transform_ghost_visual); + + void HandleAfterHit() + { + Unit* target = GetHitUnit(); + if (!target) + return; + + target->SendPlaySpellVisual(SPELL_TRANSFORM_IMPACTKIT); //SPELL_TRANSFORM_GHOST IMPACTKIT 500 + } + + void Register() override + { + AfterHit += SpellHitFn(spell_transform_ghost_visual::HandleAfterHit); + } +}; + void AddSC_instance_scarlet_monastery() { new instance_scarlet_monastery(); + RegisterSpellScript(spell_forgiveness_dummy_visual); + RegisterSpellScript(spell_transform_ghost_visual); } From b011852698d2771b548a216433a32d5a209081d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 19:48:41 +0000 Subject: [PATCH 11/69] chore(DB): import pending files Referenced commit(s): 7069d99dca92ef21eaf9cb34a72a825470a78414 --- .../rev_1783128746972420200.sql => db_world/2026_07_25_01.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1783128746972420200.sql => db_world/2026_07_25_01.sql} (96%) diff --git a/data/sql/updates/pending_db_world/rev_1783128746972420200.sql b/data/sql/updates/db_world/2026_07_25_01.sql similarity index 96% rename from data/sql/updates/pending_db_world/rev_1783128746972420200.sql rename to data/sql/updates/db_world/2026_07_25_01.sql index bc5e22aa6..f992603f1 100644 --- a/data/sql/updates/pending_db_world/rev_1783128746972420200.sql +++ b/data/sql/updates/db_world/2026_07_25_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_00 -> 2026_07_25_01 -- SPELL_TRANSFORM_GHOST and SPELL_FORGIVENESS DELETE FROM `spell_script_names` WHERE `spell_id` IN (28443, 28697); From 4bc5d1eeafffe34d8bb6ee65b7e56f86422c60f2 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:02:39 -0300 Subject: [PATCH 12/69] fix(DB/Quest): Remove Redundant Quest Credit in Rescue from Town Square (#26797) --- data/sql/updates/pending_db_world/rev_1784999317478287400.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784999317478287400.sql diff --git a/data/sql/updates/pending_db_world/rev_1784999317478287400.sql b/data/sql/updates/pending_db_world/rev_1784999317478287400.sql new file mode 100644 index 000000000..bd6c2b56d --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784999317478287400.sql @@ -0,0 +1,2 @@ +-- +UPDATE `creature_template` SET `KillCredit1` = 0 WHERE (`entry` = 27370); From 7f8955e7404dc503a57edbc0b4f6c988205bd08b Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:02:57 -0300 Subject: [PATCH 13/69] fix(DB/Quest): Update quest_offer_rewards values for quest End of the Line Horde version (#26798) --- data/sql/updates/pending_db_world/rev_1785000745157250500.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785000745157250500.sql diff --git a/data/sql/updates/pending_db_world/rev_1785000745157250500.sql b/data/sql/updates/pending_db_world/rev_1785000745157250500.sql new file mode 100644 index 000000000..5e77636c1 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785000745157250500.sql @@ -0,0 +1,2 @@ +-- +UPDATE `quest_offer_reward` SET `Emote1` = 5, `Emote2` = 1, `Emote3` = 1, `EmoteDelay2` = 1000, `RewardText` = 'WHAT?!$B$B$B$BOf course, we should have guessed that this was what they were up to all along. Quickly, there\'s no time to waste.$B$BWyrmrest must be warned!' WHERE (`ID` = 12110); From 5485cebf4143be7d355ce2a2f5b235e656a99392 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:03:09 -0300 Subject: [PATCH 14/69] fix(DB/Creature): Sniff Bjomolf (#26800) --- .../rev_1785003613580466900.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785003613580466900.sql diff --git a/data/sql/updates/pending_db_world/rev_1785003613580466900.sql b/data/sql/updates/pending_db_world/rev_1785003613580466900.sql new file mode 100644 index 000000000..34ea23d5a --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785003613580466900.sql @@ -0,0 +1,29 @@ +-- +DELETE FROM `creature` WHERE (`id` = 24516) AND (`guid` IN (114439, 114440)); +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`, `CreateObject`, `VerifiedBuild`) VALUES +(114439, 24516, 571, 0, 0, 1, 1, 0, 2319.6106, -3020.8176, 136.57187, 2.20952, 300, 0, 0, 0, 0, 2, 0, 0, 0, '', NULL, 1, 65512); + +DELETE FROM `creature_addon` WHERE `guid` = 114439; +INSERT INTO `creature_addon` (`guid`, `path_id`) VALUES +(114439, 1144390); + +DELETE FROM `waypoint_data` WHERE `id` = 1144390; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`) VALUES +(1144390, 1 , 2319.6106, -3020.8176, 136.57187, NULL), +(1144390, 2 , 2293.5505, -3033.0613, 136.70703, NULL), +(1144390, 3 , 2270.9585, -3049.1892, 136.7957, NULL), +(1144390, 4 , 2261.9817, -3069.591, 137.1125, NULL), +(1144390, 5 , 2244.4805, -3111.4001, 136.79797, NULL), +(1144390, 6 , 2234.3372, -3129.741, 138.23094, NULL), +(1144390, 7 , 2205.1501, -3149.454, 140.47078, NULL), +(1144390, 8 , 2178.7832, -3143.6543, 139.04956, NULL), +(1144390, 9 , 2159.131, -3118.843, 138.94107, NULL), +(1144390, 10, 2157.4236, -3090.9448, 138.66618, NULL), +(1144390, 11, 2184.6926, -3076.7776, 137.69681, NULL), +(1144390, 12, 2205.2817, -3067.4485, 138.47263, NULL), +(1144390, 13, 2241.1084, -3040.399, 136.02086, NULL), +(1144390, 14, 2251.2544, -3001.0442, 135.393, NULL), +(1144390, 15, 2261.8438, -2975.5894, 137.61548, NULL), +(1144390, 16, 2288.5696, -2967.9568, 134.09793, NULL), +(1144390, 17, 2312.0784, -2973.8508, 131.73712, NULL), +(1144390, 18, 2327.24, -2996.011, 135.02109, NULL); From 9416d4a0b50636e6fc1a689f6606e5ff576bdee6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 20:03:56 +0000 Subject: [PATCH 15/69] chore(DB): import pending files Referenced commit(s): 4bc5d1eeafffe34d8bb6ee65b7e56f86422c60f2 --- .../rev_1784999317478287400.sql => db_world/2026_07_25_02.sql} | 1 + .../rev_1785000745157250500.sql => db_world/2026_07_25_03.sql} | 1 + .../rev_1785003613580466900.sql => db_world/2026_07_25_04.sql} | 1 + 3 files changed, 3 insertions(+) rename data/sql/updates/{pending_db_world/rev_1784999317478287400.sql => db_world/2026_07_25_02.sql} (63%) rename data/sql/updates/{pending_db_world/rev_1785000745157250500.sql => db_world/2026_07_25_03.sql} (89%) rename data/sql/updates/{pending_db_world/rev_1785003613580466900.sql => db_world/2026_07_25_04.sql} (97%) diff --git a/data/sql/updates/pending_db_world/rev_1784999317478287400.sql b/data/sql/updates/db_world/2026_07_25_02.sql similarity index 63% rename from data/sql/updates/pending_db_world/rev_1784999317478287400.sql rename to data/sql/updates/db_world/2026_07_25_02.sql index bd6c2b56d..018e4ae17 100644 --- a/data/sql/updates/pending_db_world/rev_1784999317478287400.sql +++ b/data/sql/updates/db_world/2026_07_25_02.sql @@ -1,2 +1,3 @@ +-- DB update 2026_07_25_01 -> 2026_07_25_02 -- UPDATE `creature_template` SET `KillCredit1` = 0 WHERE (`entry` = 27370); diff --git a/data/sql/updates/pending_db_world/rev_1785000745157250500.sql b/data/sql/updates/db_world/2026_07_25_03.sql similarity index 89% rename from data/sql/updates/pending_db_world/rev_1785000745157250500.sql rename to data/sql/updates/db_world/2026_07_25_03.sql index 5e77636c1..1dbb9facd 100644 --- a/data/sql/updates/pending_db_world/rev_1785000745157250500.sql +++ b/data/sql/updates/db_world/2026_07_25_03.sql @@ -1,2 +1,3 @@ +-- DB update 2026_07_25_02 -> 2026_07_25_03 -- UPDATE `quest_offer_reward` SET `Emote1` = 5, `Emote2` = 1, `Emote3` = 1, `EmoteDelay2` = 1000, `RewardText` = 'WHAT?!$B$B$B$BOf course, we should have guessed that this was what they were up to all along. Quickly, there\'s no time to waste.$B$BWyrmrest must be warned!' WHERE (`ID` = 12110); diff --git a/data/sql/updates/pending_db_world/rev_1785003613580466900.sql b/data/sql/updates/db_world/2026_07_25_04.sql similarity index 97% rename from data/sql/updates/pending_db_world/rev_1785003613580466900.sql rename to data/sql/updates/db_world/2026_07_25_04.sql index 34ea23d5a..7e141cc22 100644 --- a/data/sql/updates/pending_db_world/rev_1785003613580466900.sql +++ b/data/sql/updates/db_world/2026_07_25_04.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_03 -> 2026_07_25_04 -- DELETE FROM `creature` WHERE (`id` = 24516) AND (`guid` IN (114439, 114440)); 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`, `CreateObject`, `VerifiedBuild`) VALUES From db165fc26aa7ca077e36282640356a28eb102ae3 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:16:58 -0300 Subject: [PATCH 16/69] fix(Core/Collision): make spawned WMO gameobjects provide indoor/area info (#26790) --- src/common/Collision/Models/GameObjectModel.cpp | 5 ++++- src/common/Collision/Models/GameObjectModel.h | 1 + src/server/game/Entities/GameObject/GameObject.cpp | 1 + src/server/game/Maps/Map.cpp | 8 +++----- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/common/Collision/Models/GameObjectModel.cpp b/src/common/Collision/Models/GameObjectModel.cpp index 5077439cd..c9883a27e 100644 --- a/src/common/Collision/Models/GameObjectModel.cpp +++ b/src/common/Collision/Models/GameObjectModel.cpp @@ -198,7 +198,8 @@ bool GameObjectModel::intersectRay(G3D::Ray const& ray, float& MaxDist, bool Sto bool GameObjectModel::GetLocationInfo(G3D::Vector3 const& point, VMAP::LocationInfo& info, uint32 ph_mask) const { - if (!(phasemask & ph_mask) || !owner->IsSpawned() || !IsMapObject()) + // transports provide inaccurate area info while moving (e.g. Booty Bay - Ratchet boat, see #7335) + if (!(phasemask & ph_mask) || !owner->IsSpawned() || !IsMapObject() || owner->IsTransport()) return false; if (!iBound.contains(point)) @@ -216,6 +217,8 @@ bool GameObjectModel::GetLocationInfo(G3D::Vector3 const& point, VMAP::LocationI float world_Z = ((modelGround * iInvRot) * iScale + iPos).z; if (info.ground_Z < world_Z) { + info.rootId = groupInfo.rootId; + info.hitModel = groupInfo.hitModel; info.ground_Z = world_Z; return true; } diff --git a/src/common/Collision/Models/GameObjectModel.h b/src/common/Collision/Models/GameObjectModel.h index 650978c19..c4142728a 100644 --- a/src/common/Collision/Models/GameObjectModel.h +++ b/src/common/Collision/Models/GameObjectModel.h @@ -42,6 +42,7 @@ public: virtual ~GameObjectModelOwnerBase() = default; [[nodiscard]] virtual bool IsSpawned() const = 0; + [[nodiscard]] virtual bool IsTransport() const { return false; } [[nodiscard]] virtual uint32 GetDisplayId() const = 0; [[nodiscard]] virtual uint32 GetPhaseMask() const = 0; [[nodiscard]] virtual G3D::Vector3 GetPosition() const = 0; diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index dbfc27b93..d30fb33ad 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -2968,6 +2968,7 @@ public: explicit GameObjectModelOwnerImpl(GameObject* owner) : _owner(owner) { } bool IsSpawned() const override { return _owner->isSpawned(); } + bool IsTransport() const override { return _owner->IsTransport(); } uint32 GetDisplayId() const override { return _owner->GetDisplayId(); } uint32 GetPhaseMask() const override { return (_owner->GetGoState() == GO_STATE_READY || _owner->IsTransport()) ? _owner->GetPhaseMask() : 0; } G3D::Vector3 GetPosition() const override { return G3D::Vector3(_owner->GetPositionX(), _owner->GetPositionY(), _owner->GetPositionZ()); } diff --git a/src/server/game/Maps/Map.cpp b/src/server/game/Maps/Map.cpp index d5563c6b8..cc045f67f 100644 --- a/src/server/game/Maps/Map.cpp +++ b/src/server/game/Maps/Map.cpp @@ -1389,15 +1389,15 @@ LiquidData const Map::GetLiquidData(uint32 phaseMask, float x, float y, float z, return liquidData; } -void Map::GetFullTerrainStatusForPosition(uint32 /*phaseMask*/, float x, float y, float z, float collisionHeight, PositionFullTerrainStatus& data, Optional reqLiquidType) +void Map::GetFullTerrainStatusForPosition(uint32 phaseMask, float x, float y, float z, float collisionHeight, PositionFullTerrainStatus& data, Optional reqLiquidType) { GridTerrainData* gmap = GetGridTerrainData(x, y); VMAP::AreaAndLiquidData vmapData; - // VMAP::AreaAndLiquidData dynData; + VMAP::AreaAndLiquidData dynData; VMAP::AreaAndLiquidData* wmoData = nullptr; _mapCollisionData.GetStaticTree().GetAreaAndLiquidData(x, y, z, reqLiquidType, vmapData); - // _dynamicTree.GetAreaAndLiquidData(x, y, z, phaseMask, reqLiquidType, dynData); + _mapCollisionData.GetDynamicTree().GetAreaAndLiquidData(x, y, z, phaseMask, reqLiquidType, dynData); uint32 gridAreaId = 0; float gridMapHeight = INVALID_HEIGHT; @@ -1424,7 +1424,6 @@ void Map::GetFullTerrainStatusForPosition(uint32 /*phaseMask*/, float x, float y // NOTE: Objects will not detect a case when a wmo providing area/liquid despawns from under them // but this is fine as these kind of objects are not meant to be spawned and despawned a lot // example: Lich King platform - /* if (dynData.floorZ > VMAP_INVALID_HEIGHT && G3D::fuzzyGe(z, dynData.floorZ - GROUND_HEIGHT_TOLERANCE) && (G3D::fuzzyLt(z, gridMapHeight - GROUND_HEIGHT_TOLERANCE) || dynData.floorZ > gridMapHeight) && (G3D::fuzzyLt(z, vmapData.floorZ - GROUND_HEIGHT_TOLERANCE) || dynData.floorZ > vmapData.floorZ)) @@ -1432,7 +1431,6 @@ void Map::GetFullTerrainStatusForPosition(uint32 /*phaseMask*/, float x, float y data.floorZ = dynData.floorZ; wmoData = &dynData; } - */ if (wmoData) { From d37d5c2f8333d9a570b61e74488a406659cf8819 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:20:08 -0300 Subject: [PATCH 17/69] fix(Core/Spells): Sanctified/Swift Retribution require an active paladin aura (#26793) --- .../rev_1784986738601277200.sql | 6 ++ src/server/scripts/Spells/spell_paladin.cpp | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784986738601277200.sql diff --git a/data/sql/updates/pending_db_world/rev_1784986738601277200.sql b/data/sql/updates/pending_db_world/rev_1784986738601277200.sql new file mode 100644 index 000000000..6c4cda52c --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784986738601277200.sql @@ -0,0 +1,6 @@ +-- +DELETE FROM `spell_script_names` WHERE `spell_id` IN (63510, 63514, 63531); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(63510, 'spell_pal_improved_concentraction_aura_effect'), +(63514, 'spell_pal_improved_devotion_aura_effect'), +(63531, 'spell_pal_sanctified_retribution_effect'); diff --git a/src/server/scripts/Spells/spell_paladin.cpp b/src/server/scripts/Spells/spell_paladin.cpp index 5b33c2fc9..3c5fe2206 100644 --- a/src/server/scripts/Spells/spell_paladin.cpp +++ b/src/server/scripts/Spells/spell_paladin.cpp @@ -71,7 +71,13 @@ enum PaladinSpells SPELL_PALADIN_SEAL_OF_RIGHTEOUSNESS = 25742, + SPELL_PALADIN_DEVOTION_AURA_R1 = 465, + SPELL_PALADIN_RETRIBUTION_AURA_R1 = 7294, SPELL_PALADIN_CONCENTRACTION_AURA = 19746, + SPELL_PALADIN_SHADOW_RESISTANCE_AURA_R1 = 19876, + SPELL_PALADIN_FROST_RESISTANCE_AURA_R1 = 19888, + SPELL_PALADIN_FIRE_RESISTANCE_AURA_R1 = 19891, + SPELL_PALADIN_CRUSADER_AURA = 32223, SPELL_PALADIN_SANCTIFIED_RETRIBUTION_R1 = 31869, SPELL_PALADIN_SWIFT_RETRIBUTION_R1 = 53379, @@ -2133,6 +2139,70 @@ private: uint32 _spellId; }; +// 63510 - Improved Concentration Aura (requires Concentration Aura on the target) +// 63514 - Improved Devotion Aura (requires Devotion Aura on the target) +class spell_pal_improved_aura_effect : public AuraScript +{ + PrepareAuraScript(spell_pal_improved_aura_effect); + +public: + spell_pal_improved_aura_effect(uint32 auraSpellId) : AuraScript(), _auraSpellId(auraSpellId) { } + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ _auraSpellId }); + } + + bool CheckAreaTarget(Unit* target) + { + return target->GetAuraOfRankedSpell(_auraSpellId, GetCasterGUID()); + } + + void Register() override + { + DoCheckAreaTarget += AuraCheckAreaTargetFn(spell_pal_improved_aura_effect::CheckAreaTarget); + } + +private: + uint32 _auraSpellId; +}; + +// 63531 - Sanctified Retribution +// "Targets affected by any of your auras" - shared effect of Sanctified Retribution and Swift Retribution +class spell_pal_sanctified_retribution_effect : public AuraScript +{ + PrepareAuraScript(spell_pal_sanctified_retribution_effect); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo( + { + SPELL_PALADIN_DEVOTION_AURA_R1, + SPELL_PALADIN_RETRIBUTION_AURA_R1, + SPELL_PALADIN_CONCENTRACTION_AURA, + SPELL_PALADIN_SHADOW_RESISTANCE_AURA_R1, + SPELL_PALADIN_FROST_RESISTANCE_AURA_R1, + SPELL_PALADIN_FIRE_RESISTANCE_AURA_R1, + SPELL_PALADIN_CRUSADER_AURA + }); + } + + bool CheckAreaTarget(Unit* target) + { + for (uint32 auraSpellId : { SPELL_PALADIN_DEVOTION_AURA_R1, SPELL_PALADIN_RETRIBUTION_AURA_R1, SPELL_PALADIN_CONCENTRACTION_AURA, + SPELL_PALADIN_SHADOW_RESISTANCE_AURA_R1, SPELL_PALADIN_FROST_RESISTANCE_AURA_R1, SPELL_PALADIN_FIRE_RESISTANCE_AURA_R1, SPELL_PALADIN_CRUSADER_AURA }) + if (target->GetAuraOfRankedSpell(auraSpellId, GetCasterGUID())) + return true; + + return false; + } + + void Register() override + { + DoCheckAreaTarget += AuraCheckAreaTargetFn(spell_pal_sanctified_retribution_effect::CheckAreaTarget); + } +}; + // 53651 - Light's Beacon - Beacon of Light // Each source heal has a dedicated beacon copy spell: // 53652 - Holy Light, 53653 - Flash of Light, 53654 - Holy Shock @@ -2256,5 +2326,8 @@ void AddSC_paladin_spell_scripts() RegisterSpellScriptWithArgs(spell_pal_improved_aura, "spell_pal_improved_devotion_aura", SPELL_PALADIN_IMPROVED_DEVOTION_AURA); RegisterSpellScriptWithArgs(spell_pal_improved_aura, "spell_pal_sanctified_retribution", SPELL_PALADIN_SANCTIFIED_RETRIBUTION_AURA); RegisterSpellScriptWithArgs(spell_pal_improved_aura, "spell_pal_swift_retribution", SPELL_PALADIN_SANCTIFIED_RETRIBUTION_AURA); + RegisterSpellScriptWithArgs(spell_pal_improved_aura_effect, "spell_pal_improved_concentraction_aura_effect", SPELL_PALADIN_CONCENTRACTION_AURA); + RegisterSpellScriptWithArgs(spell_pal_improved_aura_effect, "spell_pal_improved_devotion_aura_effect", SPELL_PALADIN_DEVOTION_AURA_R1); + RegisterSpellScript(spell_pal_sanctified_retribution_effect); RegisterSpellScript(spell_pal_light_s_beacon); } From 362e39903ffbbaa857d7b1420760e5782b79bb1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 20:21:26 +0000 Subject: [PATCH 18/69] chore(DB): import pending files Referenced commit(s): d37d5c2f8333d9a570b61e74488a406659cf8819 --- .../rev_1784986738601277200.sql => db_world/2026_07_25_05.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784986738601277200.sql => db_world/2026_07_25_05.sql} (87%) diff --git a/data/sql/updates/pending_db_world/rev_1784986738601277200.sql b/data/sql/updates/db_world/2026_07_25_05.sql similarity index 87% rename from data/sql/updates/pending_db_world/rev_1784986738601277200.sql rename to data/sql/updates/db_world/2026_07_25_05.sql index 6c4cda52c..bf59aa0d3 100644 --- a/data/sql/updates/pending_db_world/rev_1784986738601277200.sql +++ b/data/sql/updates/db_world/2026_07_25_05.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_04 -> 2026_07_25_05 -- DELETE FROM `spell_script_names` WHERE `spell_id` IN (63510, 63514, 63531); INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES From 0b2d522089b3b02b0c19733ffeef8d23424b9578 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:33:35 +0200 Subject: [PATCH 19/69] refactor(DB/Spells): Move custom attributes from core to database (#26375) --- .../rev_1782660426082411800.sql | 101 +++++++ src/server/game/Spells/SpellMgr.cpp | 265 +----------------- 2 files changed, 102 insertions(+), 264 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1782660426082411800.sql diff --git a/data/sql/updates/pending_db_world/rev_1782660426082411800.sql b/data/sql/updates/pending_db_world/rev_1782660426082411800.sql new file mode 100644 index 000000000..b3dddf478 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1782660426082411800.sql @@ -0,0 +1,101 @@ +DELETE FROM `spell_custom_attr` WHERE `spell_id` IN (6197, 11971, 12579, 14183, 15502, 17962, 27891, 28969, 29306, 29325, 30659, 32593, 32594, 34655, 35859, 38044, 38449, 40327, 40520, 43138, 45145, 45271, 45347, 45348, 45537, 49283, 49284, 49381, 49882, 50315, 50526, 52752, 53353, 54314, 54331, 54714, 55093, 55604, 55645, 56098, 58567, 58690, 58984, 59283, 59354, 59805, 61920, 62775, 63124, 63293, 63675, 63978, 64125, 64126, 64619, 64638, 65280, 65775, 66378, 66765, 66809, 67097, 67098, 67099, 67331, 67333, 67721, 67847, 67861, 67862, 67863, 68873, 70324, 71157, 72347, 72409, 72447, 72448, 72449, 72465, 72963, 72964, 72965, 72966, 74507); +INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES +-- SPELL_ATTR0_CU_CONE_BACK (0x2) +(58690, 2), -- Cyanigosa, Tail Sweep +(59283, 2), -- Cyanigosa, Tail Sweep +-- SPELL_ATTR0_CU_CONE_LINE (0x4) +(63293, 4), -- Mimiron - spinning damage +(64619, 4), -- Ulduar, Mimiron, Emergency Fire Bot, Water Spray +(68873, 4), -- Wailing Souls +(70324, 4), -- Wailing Souls +-- SPELL_ATTR0_CU_SHARE_DAMAGE (0x8) +(66765, 8), -- Meteor Fists +(66809, 8), -- Meteor Fists +(67331, 8), -- Meteor Fists +(67333, 8), -- Meteor Fists +-- SPELL_ATTR0_CU_NO_INITIAL_THREAT (0x10) +(6197, 16), -- Eagle Eye +-- SPELL_ATTR0_CU_DONT_BREAK_STEALTH (0x40) +(14183, 64), -- Premeditation +-- SPELL_ATTR0_CU_NO_PVP_FLAG (0x80) +(50315, 128), -- Disco Ball +(58984, 128), -- Shadowmeld, issue #16626 +-- SPELL_ATTR0_CU_IGNORE_EVADE (0x800) +(45537, 2048), -- Cosmetic - Lightning Beam Channel +-- SPELL_ATTR0_CU_NEGATIVE_EFF0 (0x1000) +(72347, 4096), -- Lock Players and Tap Chest (Gunship Battle) +-- SPELL_ATTR0_CU_IGNORE_ARMOR (0x8000) +(49882, 32768), -- Leviroth Self-Impale +(62775, 32768), -- Ulduar: XT-002 Tympanic Tamparum +(64125, 32768), -- Ulduar, Yogg-Saron, Squeeze +(64126, 32768), -- Ulduar, Yogg-Saron, Squeeze +(66378, 32768), -- Trial of the Crusader, Jaraxxus, Shivan Slash +(67097, 32768), -- Trial of the Crusader, Jaraxxus, Shivan Slash +(67098, 32768), -- Trial of the Crusader, Jaraxxus, Shivan Slash +(67099, 32768), -- Trial of the Crusader, Jaraxxus, Shivan Slash +(72409, 32768), -- Rune of Blood (Deathbringer Saurfang) +(72447, 32768), -- Rune of Blood (Deathbringer Saurfang) +(72448, 32768), -- Rune of Blood (Deathbringer Saurfang) +(72449, 32768), -- Rune of Blood (Deathbringer Saurfang) +-- SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET (0x20000) +(63124, 131072), -- quest There's Something About the Squire (13654) +-- SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET (0x40000) +(43138, 262144), -- North Fleet Reservist Kill Credit +-- SPELL_ATTR0_CU_BINARY_SPELL (0x100000) +(45145, 1048576), -- Snake Trap Effect +-- SPELL_ATTR0_CU_NO_POSITIVE_TAKEN_BONUS (0x200000) +(17962, 2097152), -- Conflagrate +(32593, 2097152), -- Earth Shield aura +(32594, 2097152), -- Earth Shield aura +(49283, 2097152), -- Earth Shield aura +(49284, 2097152), -- Earth Shield aura +(50526, 2097152), -- Wandering Plague +(52752, 2097152), -- Ancestral Awakening Heal +(53353, 2097152), -- Chimera Shot - Serpent trigger +(63675, 2097152), -- Improved Devouring Plague +-- SPELL_ATTR0_CU_SINGLE_AURA_STACK (0x400000) +(11971, 4194304), -- Sunder Armor +(12579, 4194304), -- Player Winter's Chill +(15502, 4194304), -- Sunder Armor +(27891, 4194304), -- Naxxramas, Sludge Belcher, Acidic Sludge (10 normal) +(28969, 4194304), -- Naxxramas, Crypt Guard, Acid Spit (10 normal) +(29306, 4194304), -- Naxxramas (Gluth's Zombies): Infected Wound +(29325, 4194304), -- Naxxramas, Stoneskin Gargoyle, Acid Volley (10 normal) +(30659, 4194304), -- Hellfire Ramparts, Fel Infusion +(34655, 4194304), -- Snake Trap, Deadly Poison +(35859, 4194304), -- The Eye, Nether Vapor +(38044, 4194304), -- Serpentshrine Cavern, Surge +(38449, 4194304), -- Serpentshrine Cavern, Blessing of the Tides +(40327, 4194304), -- Black Temple, Atrophy +(40520, 4194304), -- Black Temple, Shade Soul Channel +(45271, 4194304), -- Sunwell, Eredar Twins encounter, Dark Strike +(45347, 4194304), -- Sunwell, Eredar Twins encounter, Dark Touched +(45348, 4194304), -- Sunwell, Eredar Twins encounter, Flame Touched +(49381, 4194304), -- Drak'tharon Keep, Consume +(54314, 4194304), -- Azjol'Nerub Drain Power +(54331, 4194304), -- Naxxramas, Sludge Belcher, Acidic Sludge (25 normal) +(54714, 4194304), -- Naxxramas, Stoneskin Gargoyle, Acid Volley (25 normal) +(55093, 4194304), -- Gundrak, Grip of Slad'ran +(55604, 4194304), -- Naxxramas, Unrelenting Trainee, Death Plague (10 normal) +(55645, 4194304), -- Naxxramas, Unrelenting Trainee, Death Plague (25 normal) +(56098, 4194304), -- Naxxramas, Crypt Guard, Acid Spit (25 normal) +(58567, 4194304), -- Player Sunder Armor +(59354, 4194304), -- Azjol'Nerub Drain Power +(59805, 4194304), -- Drak'tharon Keep, Consume +(61920, 4194304), -- Ulduar (Spellbreaker): Supercharge +(63978, 4194304), -- Ulduar (Rubble): Stone Nova +(64638, 4194304), -- Ulduar, Winter Jormungar, Acidic Bite +(65280, 4194304), -- Ulduar, Hodir, Singed +(65775, 4194304), -- Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (10 normal) +(67721, 4194304), -- Anub'arak, Nerubian Burrower, Expose Weakness (normal) +(67847, 4194304), -- Anub'arak, Nerubian Burrower, Expose Weakness (heroic) +(67861, 4194304), -- Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (25 normal) +(67862, 4194304), -- Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (10 heroic) +(67863, 4194304), -- Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (25 heroic) +(71157, 4194304), -- Icecrown Citadel, Plagued Zombie, Infected Wound +(72465, 4194304), -- Icecrown Citadel, Sindragosa, Respite for a Tormented Soul (weekly quest) +(72963, 4194304), -- Icecrown Citadel, Rot Worm, Flesh Rot (10 normal) +(72964, 4194304), -- Icecrown Citadel, Rot Worm, Flesh Rot (25 normal) +(72965, 4194304), -- Icecrown Citadel, Rot Worm, Flesh Rot (10 heroic) +(72966, 4194304), -- Icecrown Citadel, Rot Worm, Flesh Rot (25 heroic) +(74507, 4194304); -- Ruby Sanctum, Siphoned Might diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 1f23766ea..b05f69aae 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -3507,268 +3507,6 @@ void SpellMgr::LoadSpellInfoCustomAttributes() switch (spellInfo->Id) { - // Xinef: additional spells which should be binary - case 45145: // Snake Trap Effect - spellInfo->AttributesCu |= SPELL_ATTR0_CU_BINARY_SPELL; - break; - case 1776: // Gouge - case 1777: - case 8629: - case 11285: - case 11286: - case 12540: - case 13579: - case 24698: - case 28456: - case 29425: - case 34940: - case 36862: - case 38764: - case 38863: - case 52743: // Head Smack - spellInfo->AttributesCu |= SPELL_ATTR0_CU_REQ_TARGET_FACING_CASTER; - break; - case 53: // Backstab - case 2589: - case 2590: - case 2591: - case 7159: - case 8627: - case 8721: - case 11279: - case 11280: - case 11281: - case 15582: - case 15657: - case 22416: - case 25300: - case 26863: - case 37685: - case 48656: - case 48657: - case 703: // Garrote - case 8631: - case 8632: - case 8633: - case 11289: - case 11290: - case 26839: - case 26884: - case 48675: - case 48676: - case 5221: // Shred - case 6800: - case 8992: - case 9829: - case 9830: - case 27001: - case 27002: - case 48571: - case 48572: - case 8676: // Ambush - case 8724: - case 8725: - case 11267: - case 11268: - case 11269: - case 27441: - case 48689: - case 48690: - case 48691: - case 6785: // Ravage - case 6787: - case 9866: - case 9867: - case 27005: - case 48578: - case 48579: - case 21987: // Lash of Pain - case 23959: // Test Stab R50 - case 24825: // Test Backstab - case 58563: // Assassinate Restless Lookout - case 63124: // quest There's Something About the Squire (13654) - spellInfo->AttributesCu |= SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET; - break; - case 26029: // Dark Glare - case 43140: // Flame Breath - case 43215: // Flame Breath - case 70461: // Coldflame Trap - case 72133: // Pain and Suffering - case 73788: // Pain and Suffering - case 73789: // Pain and Suffering - case 73790: // Pain and Suffering - case 63293: // Mimiron - spinning damage - case 68873: // Wailing Souls - case 70324: // Wailing Souls - case 64619: // Ulduar, Mimiron, Emergency Fire Bot, Water Spray - spellInfo->AttributesCu |= SPELL_ATTR0_CU_CONE_LINE; - break; - case 58690: // Cyanigosa, Tail Sweep - case 59283: // Cyanigosa, Tail Sweep - spellInfo->AttributesCu |= SPELL_ATTR0_CU_CONE_BACK; - break; - case 24340: // Meteor - case 26558: // Meteor - case 28884: // Meteor - case 36837: // Meteor - case 38903: // Meteor - case 41276: // Meteor - case 57467: // Meteor - case 26789: // Shard of the Fallen Star - case 31436: // Malevolent Cleave - case 40810: // Saber Lash - case 43267: // Saber Lash - case 43268: // Saber Lash - case 42384: // Brutal Swipe - case 45150: // Meteor Slash - case 64688: // Sonic Screech - case 72373: // Shared Suffering - case 71904: // Chaos Bane - case 70492: // Ooze Eruption - case 72505: // Ooze Eruption - case 72624: // Ooze Eruption - case 72625: // Ooze Eruption - // ONLY SPELLS WITH SPELLFAMILY_GENERIC and EFFECT_SCHOOL_DAMAGE, OR WEAPON_DMG_X - case 66809: // Meteor Fists - case 67331: // Meteor Fists - case 66765: // Meteor Fists - case 67333: // Meteor Fists - spellInfo->AttributesCu |= SPELL_ATTR0_CU_SHARE_DAMAGE; - break; - case 18500: // Wing Buffet - case 33086: // Wild Bite - case 49749: // Piercing Blow - case 52890: // Penetrating Strike - case 53454: // Impale - case 59446: // Impale - case 62383: // Shatter - case 64777: // Machine Gun - case 65239: // Machine Gun - case 69293: // Wing Buffet - case 74439: // Machine Gun - // Trial of the Crusader, Jaraxxus, Shivan Slash - case 66378: - case 67097: - case 67098: - case 67099: - // Trial of the Crusader, Anub'arak, Impale - case 65919: - case 67858: - case 67859: - case 67860: - case 63278: // Mark of the Faceless (General Vezax) - case 64125: // Ulduar, Yogg-Saron, Squeeze - case 64126: // Ulduar, Yogg-Saron, Squeeze - case 62544: // Thrust (Argent Tournament) - case 64588: // Thrust (Argent Tournament) - case 66479: // Thrust (Argent Tournament) - case 68505: // Thrust (Argent Tournament) - case 62709: // Counterattack! (Argent Tournament) - case 62626: // Break-Shield (Argent Tournament, Player) - case 64590: // Break-Shield (Argent Tournament, Player) - case 64342: // Break-Shield (Argent Tournament, NPC) - case 64686: // Break-Shield (Argent Tournament, NPC) - case 65147: // Break-Shield (Argent Tournament, NPC) - case 68504: // Break-Shield (Argent Tournament, NPC) - case 62874: // Charge (Argent Tournament, Player) - case 68498: // Charge (Argent Tournament, Player) - case 64591: // Charge (Argent Tournament, Player) - case 63003: // Charge (Argent Tournament, NPC) - case 63010: // Charge (Argent Tournament, NPC) - case 68321: // Charge (Argent Tournament, NPC) - case 72255: // Mark of the Fallen Champion (Deathbringer Saurfang) - case 72444: // Mark of the Fallen Champion (Deathbringer Saurfang) - case 72445: // Mark of the Fallen Champion (Deathbringer Saurfang) - case 72446: // Mark of the Fallen Champion (Deathbringer Saurfang) - case 72409: // Rune of Blood (Deathbringer Saurfang) - case 72447: // Rune of Blood (Deathbringer Saurfang) - case 72448: // Rune of Blood (Deathbringer Saurfang) - case 72449: // Rune of Blood (Deathbringer Saurfang) - case 49882: // Leviroth Self-Impale - case 62775: // Ulduar: XT-002 Tympanic Tamparum - spellInfo->AttributesCu |= SPELL_ATTR0_CU_IGNORE_ARMOR; - break; - case 64422: // Sonic Screech (Auriaya) - spellInfo->AttributesCu |= SPELL_ATTR0_CU_SHARE_DAMAGE; - spellInfo->AttributesCu |= SPELL_ATTR0_CU_IGNORE_ARMOR; - break; - case 72293: // Mark of the Fallen Champion (Deathbringer Saurfang) - case 72347: // Lock Players and Tap Chest (Gunship Battle) - spellInfo->AttributesCu |= SPELL_ATTR0_CU_NEGATIVE_EFF0; - break; - default: - break; - case 63675: // Improved Devouring Plague - case 17962: // Conflagrate - case 32593: // Earth Shield aura - case 32594: // Earth Shield aura - case 49283: // Earth Shield aura - case 49284: // Earth Shield aura - case 50526: // Wandering Plague - case 53353: // Chimera Shot - Serpent trigger - case 52752: // Ancestral Awakening Heal - spellInfo->AttributesCu |= SPELL_ATTR0_CU_NO_POSITIVE_TAKEN_BONUS; - break; - case 65280: // Ulduar, Hodir, Singed - case 28969: // Naxxramas, Crypt Guard, Acid Spit (10 normal) - case 56098: // Naxxramas, Crypt Guard, Acid Spit (25 normal) - case 27891: // Naxxramas, Sludge Belcher, Acidic Sludge (10 normal) - case 54331: // Naxxramas, Sludge Belcher, Acidic Sludge (25 normal) - case 29325: // Naxxramas, Stoneskin Gargoyle, Acid Volley (10 normal) - case 54714: // Naxxramas, Stoneskin Gargoyle, Acid Volley (25 normal) - case 65775: // Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (10 normal) - case 67861: // Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (25 normal) - case 67862: // Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (10 heroic) - case 67863: // Anub'arak, Swarm Scarab, Acid-Drenched Mandibles (25 heroic) - case 55604: // Naxxramas, Unrelenting Trainee, Death Plague (10 normal) - case 55645: // Naxxramas, Unrelenting Trainee, Death Plague (25 normal) - case 67721: // Anub'arak, Nerubian Burrower, Expose Weakness (normal) - case 67847: // Anub'arak, Nerubian Burrower, Expose Weakness (heroic) - case 64638: // Ulduar, Winter Jormungar, Acidic Bite - case 71157: // Icecrown Citadel, Plagued Zombie, Infected Wound - case 72963: // Icecrown Citadel, Rot Worm, Flesh Rot (10 normal) - case 72964: // Icecrown Citadel, Rot Worm, Flesh Rot (25 normal) - case 72965: // Icecrown Citadel, Rot Worm, Flesh Rot (10 heroic) - case 72966: // Icecrown Citadel, Rot Worm, Flesh Rot (25 heroic) - case 72465: // Icecrown Citadel, Sindragosa, Respite for a Tormented Soul (weekly quest) - case 45271: // Sunwell, Eredar Twins encounter, Dark Strike - case 45347: // Sunwell, Eredar Twins encounter, Dark Touched - case 45348: // Sunwell, Eredar Twins encounter, Flame Touched - case 35859: // The Eye, Nether Vapor - case 40520: // Black Temple, Shade Soul Channel - case 40327: // Black Temple, Atrophy - case 38449: // Serpentshrine Cavern, Blessing of the Tides - case 38044: // Serpentshrine Cavern, Surge - case 74507: // Ruby Sanctum, Siphoned Might - case 49381: // Drak'tharon Keep, Consume - case 59805: // Drak'tharon Keep, Consume - case 55093: // Gundrak, Grip of Slad'ran - case 30659: // Hellfire Ramparts, Fel Infusion - case 54314: // Azjol'Nerub Drain Power - case 59354: // Azjol'Nerub Drain Power - case 34655: // Snake Trap, Deadly Poison - case 11971: // Sunder Armor - case 58567: // Player Sunder Armor - case 12579: // Player Winter's Chill - case 29306: // Naxxramas(Gluth's Zombies): Infected Wound - case 61920: // Ulduar(Spellbreaker): Supercharge - case 63978: // Ulduar(Rubble): Stone Nova - case 15502: // Sunder Armor - spellInfo->AttributesCu |= SPELL_ATTR0_CU_SINGLE_AURA_STACK; - break; - case 43138: // North Fleet Reservist Kill Credit - spellInfo->AttributesCu |= SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET; - break; - case 6197: // Eagle Eye - spellInfo->AttributesCu |= SPELL_ATTR0_CU_NO_INITIAL_THREAT; - break; - case 50315: // Disco Ball - spellInfo->AttributesCu |= SPELL_ATTR0_CU_NO_PVP_FLAG; - break; - case 14183: // Premeditation - spellInfo->AttributesCu |= SPELL_ATTR0_CU_DONT_BREAK_STEALTH; - break; - // Xinef: NOT CUSTOM, cant add in DBC CORRECTION because i need to swap effects, too much work to do there // Envenom case 32645: @@ -3796,8 +3534,7 @@ void SpellMgr::LoadSpellInfoCustomAttributes() case 44535: // Spirit Heal, abilities also have no cost spellInfo->Effects[EFFECT_0].MiscValue = 127; break; - case 45537: // Cosmetic - Lightning Beam Channel - spellInfo->AttributesCu |= SPELL_ATTR0_CU_IGNORE_EVADE; + default: break; } From da5d724ff092a884ef354e9cabf1d5e9241432b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 20:34:47 +0000 Subject: [PATCH 20/69] chore(DB): import pending files Referenced commit(s): 0b2d522089b3b02b0c19733ffeef8d23424b9578 --- .../rev_1782660426082411800.sql => db_world/2026_07_25_06.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1782660426082411800.sql => db_world/2026_07_25_06.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1782660426082411800.sql b/data/sql/updates/db_world/2026_07_25_06.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1782660426082411800.sql rename to data/sql/updates/db_world/2026_07_25_06.sql index b3dddf478..a19258cf5 100644 --- a/data/sql/updates/pending_db_world/rev_1782660426082411800.sql +++ b/data/sql/updates/db_world/2026_07_25_06.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_05 -> 2026_07_25_06 DELETE FROM `spell_custom_attr` WHERE `spell_id` IN (6197, 11971, 12579, 14183, 15502, 17962, 27891, 28969, 29306, 29325, 30659, 32593, 32594, 34655, 35859, 38044, 38449, 40327, 40520, 43138, 45145, 45271, 45347, 45348, 45537, 49283, 49284, 49381, 49882, 50315, 50526, 52752, 53353, 54314, 54331, 54714, 55093, 55604, 55645, 56098, 58567, 58690, 58984, 59283, 59354, 59805, 61920, 62775, 63124, 63293, 63675, 63978, 64125, 64126, 64619, 64638, 65280, 65775, 66378, 66765, 66809, 67097, 67098, 67099, 67331, 67333, 67721, 67847, 67861, 67862, 67863, 68873, 70324, 71157, 72347, 72409, 72447, 72448, 72449, 72465, 72963, 72964, 72965, 72966, 74507); INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES -- SPELL_ATTR0_CU_CONE_BACK (0x2) From e462aef6b09cefeb6c2ba1a14542a0595da18ddf Mon Sep 17 00:00:00 2001 From: Xepic90 Date: Sat, 25 Jul 2026 16:41:21 -0400 Subject: [PATCH 21/69] fix(Scripts/Ulduar): Algalon defeat, wipe recovery, and Brann's pathfinding to him (#26684) Co-authored-by: Si1ker <61223313+Si1ker@users.noreply.github.com> Co-authored-by: sogladev --- .../Ulduar/boss_algalon_the_observer.cpp | 185 +++++++++++------- .../Ulduar/Ulduar/instance_ulduar.cpp | 15 +- .../scripts/Northrend/Ulduar/Ulduar/ulduar.h | 3 + 3 files changed, 134 insertions(+), 69 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp index ed3024c2d..b5d1f8b4b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp @@ -25,6 +25,7 @@ #include "PassiveAI.h" #include "Player.h" #include "ScriptedCreature.h" +#include "ScriptMgr.h" #include "SpellScript.h" #include "SpellScriptLoader.h" #include "ulduar.h" @@ -78,7 +79,7 @@ enum Actions { //ACTION_INIT_ALGALON = 1, defined in ulduar.h //ACTION_DESPAWN_ALGALON = 2, defined in ulduar.h - ACTION_START_INTRO = 3, + //ACTION_START_INTRO = 3, defined in ulduar.h ACTION_FINISH_INTRO = 4, ACTION_ACTIVATE_STAR = 5, ACTION_BIG_BANG = 6, @@ -95,6 +96,7 @@ enum Misc POINT_ALGALON_LAND = 1, POINT_ALGALON_OUTRO = 2, + POINT_ALGALON_FLOAT = 3, EVENT_ID_SUPERMASSIVE_START = 21697, @@ -112,46 +114,51 @@ enum Events EVENT_SUMMON_ALGALON = 3, EVENT_BRANN_OUTRO_1 = 4, EVENT_BRANN_OUTRO_2 = 5, + EVENT_BRANN_REACH_TALK_POINT = 6, // Algalon the Observer - EVENT_INTRO_1 = 6, - EVENT_INTRO_2 = 7, - EVENT_INTRO_3 = 8, - EVENT_INTRO_FINISH = 9, - EVENT_SAY_ALGALON_AGGRO = 10, - EVENT_INTRO_TIMER_DONE = 11, - EVENT_QUANTUM_STRIKE = 12, - EVENT_PHASE_PUNCH = 13, - EVENT_SUMMON_COLLAPSING_STAR = 14, - EVENT_BIG_BANG = 15, - EVENT_RESUME_UPDATING = 16, - EVENT_ASCEND_TO_THE_HEAVENS = 17, - EVENT_EVADE = 18, - EVENT_COSMIC_SMASH = 19, - EVENT_UNLOCK_YELL = 20, - EVENT_OUTRO_START = 21, - EVENT_OUTRO_1 = 22, - EVENT_OUTRO_2 = 23, - EVENT_OUTRO_3 = 24, - EVENT_OUTRO_4 = 25, - EVENT_OUTRO_5 = 26, - EVENT_OUTRO_6 = 27, - EVENT_OUTRO_7 = 28, - EVENT_OUTRO_8 = 29, - EVENT_OUTRO_9 = 30, - EVENT_OUTRO_10 = 31, - EVENT_OUTRO_11 = 32, - EVENT_ACTIVATE_LIVING_CONSTELLATION = 33, - EVENT_CHECK_HERALD_ITEMS = 34, - EVENT_ALGALON_IN_PROGRESS = 35, - EVENT_DESPAWN_ALGALON_1 = 36, - EVENT_DESPAWN_ALGALON_2 = 37, - EVENT_DESPAWN_ALGALON_3 = 38, - EVENT_DESPAWN_ALGALON_4 = 39, - EVENT_DESPAWN_ALGALON_5 = 40, + EVENT_INTRO_1 = 7, + EVENT_INTRO_2 = 8, + EVENT_INTRO_3 = 9, + EVENT_INTRO_FINISH = 10, + EVENT_SAY_ALGALON_AGGRO = 11, + EVENT_INTRO_TIMER_DONE = 12, + EVENT_QUANTUM_STRIKE = 13, + EVENT_PHASE_PUNCH = 14, + EVENT_SUMMON_COLLAPSING_STAR = 15, + EVENT_BIG_BANG = 16, + EVENT_RESUME_UPDATING = 17, + EVENT_ASCEND_TO_THE_HEAVENS = 18, + EVENT_EVADE = 19, + EVENT_COSMIC_SMASH = 20, + EVENT_UNLOCK_YELL = 21, + EVENT_OUTRO_START = 22, + EVENT_OUTRO_1 = 23, + EVENT_OUTRO_2 = 24, + EVENT_OUTRO_3 = 25, + EVENT_OUTRO_4 = 26, + EVENT_OUTRO_5 = 27, + EVENT_OUTRO_6 = 28, + EVENT_OUTRO_7 = 29, + EVENT_OUTRO_8 = 30, + EVENT_OUTRO_9 = 31, + EVENT_OUTRO_10 = 32, + EVENT_OUTRO_11 = 33, + EVENT_ACTIVATE_LIVING_CONSTELLATION = 34, + EVENT_CHECK_HERALD_ITEMS = 35, + EVENT_ALGALON_IN_PROGRESS = 36, + EVENT_DESPAWN_ALGALON_1 = 37, + EVENT_DESPAWN_ALGALON_2 = 38, + EVENT_DESPAWN_ALGALON_3 = 39, + EVENT_DESPAWN_ALGALON_4 = 40, + EVENT_DESPAWN_ALGALON_5 = 41, + + EVENT_INTRO_CHANNEL = 42, + EVENT_INTRO_SUMMON = 43, + EVENT_INTRO_DESCEND = 44, // Living Constellation - EVENT_ARCANE_BARRAGE = 41, + EVENT_ARCANE_BARRAGE = 45, }; enum EncounterPhases @@ -204,7 +211,7 @@ Position const BrannIntroWaypoint[MAX_BRANN_WAYPOINTS_INTRO] = {1632.676f, -190.5927f, 427.2631f, 0.0f}, {1631.497f, -214.2221f, 418.1152f, 0.0f}, {1636.455f, -263.6647f, 417.3213f, 0.0f}, - {1629.586f, -267.9792f, 417.3219f, 0.0f}, + {1624.1223f, -267.04172f, 417.3216f, 4.7690268f}, {1631.497f, -214.2221f, 418.1152f, 0.0f}, {1632.676f, -190.5927f, 425.8831f, 0.0f}, {1632.814f, -173.9334f, 427.2621f, 0.0f}, @@ -235,7 +242,8 @@ Position const CollapsingStarPos[COLLAPSING_STAR_COUNT] = {1622.451f, -321.1563f, 417.6188f, 4.677482f}, {1615.060f, -291.6816f, 417.7796f, 3.490659f}, }; -Position const AlgalonOutroPos = {1633.64f, -317.78f, 417.3211f, 0.0f}; +Position const AlgalonOutroPos = {1633.64f, -317.78f, 417.3211f, 1.605703f}; +Position const AlgalonFloatPos = {1632.668f, -302.7656f, 420.3211f, 1.530165f}; Position const BrannOutroPos[3] = { {1632.023f, -243.7434f, 417.9118f, 0.0f}, @@ -369,7 +377,14 @@ struct boss_algalon_the_observer : public ScriptedAI if (_instance) _instance->SetBossState(BOSS_ALGALON, FAIL); - ScriptedAI::EnterEvadeMode(why); + if (!_EnterEvadeMode(why)) + return; + + me->GetMotionMaster()->MoveTargetedHome(); + + Reset(); + + sScriptMgr->OnUnitEnterEvadeMode(me, why); } void Reset() override @@ -392,6 +407,8 @@ struct boss_algalon_the_observer : public ScriptedAI { _firstPull = false; _instance->StorePersistentData(PERSISTENT_DATA_ALGALON_FIRST_PULL, 1); + _instance->SetData(DATA_RESUMMON_ALGALON, 0); + me->DespawnOrUnsummon(1ms); } _instance->SetBossState(BOSS_ALGALON, NOT_STARTED); } @@ -416,15 +433,14 @@ struct boss_algalon_the_observer : public ScriptedAI me->SetDisableGravity(true); me->CastSpell(me, SPELL_ARRIVAL, true); me->CastSpell(me, SPELL_RIDE_THE_LIGHTNING, true); - me->GetMotionMaster()->MovePoint(POINT_ALGALON_LAND, AlgalonLandPos); + me->GetMotionMaster()->MovePoint(POINT_ALGALON_FLOAT, AlgalonFloatPos); me->SetHomePosition(AlgalonLandPos); - Movement::MoveSplineInit init(me); - init.MoveTo(AlgalonLandPos.GetPositionX(), AlgalonLandPos.GetPositionY(), AlgalonLandPos.GetPositionZ()); - init.SetOrientationFixed(true); - init.Launch(); events.Reset(); events.SetPhase(PHASE_ROLE_PLAY); events.ScheduleEvent(EVENT_INTRO_1, 5s, 0, PHASE_ROLE_PLAY); + events.ScheduleEvent(EVENT_INTRO_CHANNEL, 6s, 0, PHASE_ROLE_PLAY); + events.ScheduleEvent(EVENT_INTRO_SUMMON, 7s, 0, PHASE_ROLE_PLAY); + events.ScheduleEvent(EVENT_INTRO_DESCEND, 10s, 0, PHASE_ROLE_PLAY); events.ScheduleEvent(EVENT_INTRO_2, 15s, 0, PHASE_ROLE_PLAY); events.ScheduleEvent(EVENT_INTRO_3, 23s, 0, PHASE_ROLE_PLAY); events.ScheduleEvent(EVENT_INTRO_FINISH, 36s, 0, PHASE_ROLE_PLAY); @@ -525,18 +541,7 @@ struct boss_algalon_the_observer : public ScriptedAI if (pointId == POINT_ALGALON_LAND) me->SetDisableGravity(false); else if (pointId == POINT_ALGALON_OUTRO) - { - me->SetFacingTo(1.605703f); - events.ScheduleEvent(EVENT_OUTRO_3, 1200ms); - events.ScheduleEvent(EVENT_OUTRO_4, 2400ms); - events.ScheduleEvent(EVENT_OUTRO_5, 8500ms); - events.ScheduleEvent(EVENT_OUTRO_6, 15s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_7, 55s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_8, 73s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_9, 85s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_10, 111s); - events.ScheduleEvent(EVENT_OUTRO_11, 117s + 500ms); - } + me->SetFacingTo(AlgalonOutroPos.GetOrientation()); } void JustSummoned(Creature* summon) override @@ -645,14 +650,26 @@ struct boss_algalon_the_observer : public ScriptedAI { case EVENT_INTRO_1: me->RemoveAurasDueToSpell(SPELL_RIDE_THE_LIGHTNING); - Talk(SAY_ALGALON_INTRO_1); + if (_firstPull) + Talk(SAY_ALGALON_INTRO_1); + break; + case EVENT_INTRO_CHANNEL: + me->SetEmoteState(EMOTE_STATE_SPELL_CHANNEL_OMNI); + break; + case EVENT_INTRO_SUMMON: + me->CastSpell((Unit*)nullptr, SPELL_SUMMON_AZEROTH, true); + me->ClearEmoteState(); + break; + case EVENT_INTRO_DESCEND: + me->GetMotionMaster()->MovePoint(POINT_ALGALON_LAND, AlgalonLandPos); break; case EVENT_INTRO_2: - me->CastSpell((Unit*)nullptr, SPELL_SUMMON_AZEROTH, true); - Talk(SAY_ALGALON_INTRO_2); + if (_firstPull) + Talk(SAY_ALGALON_INTRO_2); break; case EVENT_INTRO_3: - Talk(SAY_ALGALON_INTRO_3); + if (_firstPull) + Talk(SAY_ALGALON_INTRO_3); break; case EVENT_INTRO_FINISH: events.Reset(); @@ -755,9 +772,20 @@ struct boss_algalon_the_observer : public ScriptedAI { Player* lootRecipent = me->GetLootRecipient(); _EnterEvadeMode(); + me->ClearUnitState(UNIT_STATE_EVADE); // LootRecipent is cleared in _EnterEvadeMode, restore it me->SetLootRecipient(lootRecipent); + me->GetMotionMaster()->Clear(false); me->GetMotionMaster()->MovePoint(POINT_ALGALON_OUTRO, AlgalonOutroPos); + events.ScheduleEvent(EVENT_OUTRO_3, 3200ms); + events.ScheduleEvent(EVENT_OUTRO_4, 4400ms); + events.ScheduleEvent(EVENT_OUTRO_5, 10500ms); + events.ScheduleEvent(EVENT_OUTRO_6, 17s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_7, 57s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_8, 75s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_9, 87s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_10, 113s); + events.ScheduleEvent(EVENT_OUTRO_11, 119s + 500ms); break; } case EVENT_OUTRO_3: @@ -869,16 +897,12 @@ struct npc_brann_bronzebeard_algalon : public CreatureAI delay = 8s; me->SetWalk(true); break; - case 6: - me->SetFacingTo(4.6156f); - me->SetWalk(false); - Talk(SAY_BRANN_ALGALON_INTRO_1); - events.ScheduleEvent(EVENT_SUMMON_ALGALON, 7500ms); - return; case 10: me->DespawnOrUnsummon(1ms); return; case POINT_BRANN_OUTRO: + me->SetFacingTo(4.6528215f); + return; case POINT_BRANN_OUTRO_END: return; } @@ -895,7 +919,32 @@ struct npc_brann_bronzebeard_algalon : public CreatureAI { case EVENT_BRANN_MOVE_INTRO: if (_currentPoint < MAX_BRANN_WAYPOINTS_INTRO) - me->GetMotionMaster()->MovePoint(_currentPoint, BrannIntroWaypoint[_currentPoint]); + { + if (_currentPoint == 5 || _currentPoint == 6) + { + Position const& dest = BrannIntroWaypoint[_currentPoint]; + Milliseconds travelTime = Milliseconds(uint32(me->GetExactDist2d(dest) / 2.5f * 1000)) + 500ms; + Movement::MoveSplineInit init(me); + init.MoveTo(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + init.Launch(); + if (_currentPoint == 6) + events.ScheduleEvent(EVENT_BRANN_REACH_TALK_POINT, travelTime); + else + { + _currentPoint = 6; + events.ScheduleEvent(EVENT_BRANN_MOVE_INTRO, travelTime); + } + } + else + me->GetMotionMaster()->MovePoint(_currentPoint, BrannIntroWaypoint[_currentPoint]); + } + break; + case EVENT_BRANN_REACH_TALK_POINT: + _currentPoint = 7; + me->SetFacingTo(4.7760954f); + me->SetWalk(false); + Talk(SAY_BRANN_ALGALON_INTRO_1); + events.ScheduleEvent(EVENT_SUMMON_ALGALON, 7500ms); break; case EVENT_SUMMON_ALGALON: if (me->GetInstanceScript() && !me->GetInstanceScript()->GetCreature(BOSS_ALGALON)) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index c3e04ed89..7374ef448 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -219,6 +219,7 @@ public: // Shared EventMap _events; bool _mimironTramUsed; + bool _algalonResummonPending; void Initialize() override { @@ -234,6 +235,7 @@ public: // Shared _events.Reset(); _mimironTramUsed = false; + _algalonResummonPending = false; } void FillInitialWorldStates(WorldPackets::WorldState::InitWorldStates& packet) override @@ -279,7 +281,7 @@ public: uint32 algalonTimer = GetPersistentData(PERSISTENT_DATA_ALGALON_TIMER); - if (!GetObjectGuid(BOSS_ALGALON) && algalonTimer + if (!GetObjectGuid(BOSS_ALGALON) && !_algalonResummonPending && algalonTimer && (algalonTimer <= 60 || algalonTimer == TIMER_ALGALON_TO_SUMMON)) { @@ -708,6 +710,10 @@ public: StorePersistentData(PERSISTENT_DATA_ALGALON_TIMER, 60); _events.RescheduleEvent(EVENT_UPDATE_ALGALON_TIMER, 1min); return; + case DATA_RESUMMON_ALGALON: + _algalonResummonPending = true; + _events.RescheduleEvent(EVENT_RESUMMON_ALGALON, 2s); + return; case DATA_ALGALON_SUMMON_STATE: case DATA_ALGALON_DEFEATED: DoUpdateWorldState(WORLD_STATE_ULDUAR_ALGALON_TIMER_ENABLED, 0); @@ -931,7 +937,14 @@ public: SetData(DATA_ALGALON_DEFEATED, 1); if (Creature* algalon = GetCreature(BOSS_ALGALON)) algalon->AI()->DoAction(ACTION_DESPAWN_ALGALON); + break; } + case EVENT_RESUMMON_ALGALON: + _algalonResummonPending = false; + if (!GetCreature(BOSS_ALGALON)) + if (Creature* algalon = instance->SummonCreature(NPC_ALGALON, AlgalonSummonPos)) + algalon->AI()->DoAction(ACTION_START_INTRO); + break; } } diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h index 2ec9ba83c..f2f294542 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h @@ -111,6 +111,7 @@ enum UlduarData DATA_UNIVERSE_GLOBE = 608, DATA_ALGALON_TRAPDOOR = 609, DATA_BRANN_BRONZEBEARD_ALG = 610, + DATA_RESUMMON_ALGALON = 611, // Achievements DATA_DWARFAGEDDON = 700, @@ -362,9 +363,11 @@ enum UlduarMisc ACTION_TOWER_OF_LIFE_DESTROYED = 4, EVENT_UPDATE_ALGALON_TIMER = 1, + EVENT_RESUMMON_ALGALON = 2, ACTION_FEEDS_ON_TEARS_FAILED = 0, ACTION_INIT_ALGALON = 1, ACTION_DESPAWN_ALGALON = 2, + ACTION_START_INTRO = 3, TIMER_ALGALON_DEFEATED = 300, TIMER_ALGALON_TO_SUMMON = 200, From 04f9b9f3e0426af617e79d884c864cc156b864c1 Mon Sep 17 00:00:00 2001 From: EricksOliveira Date: Sat, 25 Jul 2026 20:41:49 +0000 Subject: [PATCH 22/69] =?UTF-8?q?fix(DB/Creature):=20correct=20broken=20AI?= =?UTF-8?q?=20pathing=20for=20Kargath=20Expeditionary=E2=80=A6=20(#26709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gultask <100873791+Gultask@users.noreply.github.com> --- .../rev_1784574123694351600.sql | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784574123694351600.sql diff --git a/data/sql/updates/pending_db_world/rev_1784574123694351600.sql b/data/sql/updates/pending_db_world/rev_1784574123694351600.sql new file mode 100644 index 000000000..a153eec12 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784574123694351600.sql @@ -0,0 +1,39 @@ +-- +DELETE FROM `creature_formations` WHERE `memberGUID` IN (6877,6880,6883,6885,6886); +INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES +(6885,6877,12,180,515,0,0), +(6885,6880,9,180,515,0,0), +(6885,6883,6,180,515,0,0), +(6885,6885,0,0,515,0,0), +(6885,6886,3,180,515,0,0); + +DELETE FROM `waypoint_data` WHERE `id`=68850; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(68850,1,-6686.64,-2162.67,244.195,NULL,0,117750,0,0,0,100,0), +(68850,2,-6698.09,-2166.71,244.144,NULL,0,0,0,0,0,100,0), +(68850,3,-6724.88,-2177.67,244.144,NULL,0,0,0,0,0,100,0), +(68850,4,-6754.51,-2188.96,244.145,NULL,0,0,0,0,0,100,0), +(68850,5,-6789.04,-2200.47,244.127,NULL,0,0,0,0,0,100,0), +(68850,6,-6836.76,-2216.8,244.144,NULL,0,0,0,0,0,100,0), +(68850,7,-6842.6,-2224.08,243.735,NULL,0,0,0,0,0,100,0), +(68850,8,-6831.41,-2224.13,244.326,NULL,0,0,0,0,0,100,0), +(68850,9,-6819.23,-2217.24,244.193,NULL,0,0,0,0,0,100,0), +(68850,10,-6779.18,-2201.85,244.142,NULL,0,0,0,0,0,100,0), +(68850,11,-6742.57,-2187.83,244.144,NULL,0,0,0,0,0,100,0), +(68850,12,-6704.67,-2180.27,244.325,NULL,0,0,0,0,0,100,0), +(68850,13,-6696.46,-2187.56,247.256,NULL,0,0,0,0,0,100,0), +(68850,14,-6675.44,-2195.12,247.47,NULL,0,0,0,0,0,100,0), +(68850,15,-6657.14,-2204.38,246.199,NULL,0,0,0,0,0,100,0), +(68850,16,-6639.98,-2211.67,244.322,NULL,0,0,0,0,0,100,0), +(68850,17,-6622.15,-2237.86,244.144,NULL,0,0,0,0,0,100,0), +(68850,18,-6609.55,-2297.66,244.144,NULL,0,0,0,0,0,100,0), +(68850,19,-6602.49,-2337.11,244.564,NULL,0,0,0,0,0,100,0), +(68850,20,-6593.68,-2338.02,244.451,NULL,0,0,0,0,0,100,0), +(68850,21,-6597.48,-2311.3,244.163,NULL,0,0,0,0,0,100,0), +(68850,22,-6608.46,-2282.16,244.144,NULL,0,0,0,0,0,100,0), +(68850,23,-6615.97,-2237.53,244.144,NULL,0,0,0,0,0,100,0), +(68850,24,-6638.14,-2204.14,244.144,NULL,0,0,0,0,0,100,0), +(68850,25,-6664.19,-2186.06,244.144,NULL,0,0,0,0,0,100,0), +(68850,26,-6689.15,-2168.01,244.145,NULL,0,0,0,0,0,100,0), +(68850,27,-6690.26,-2159.42,244.145,NULL,0,0,0,0,0,100,0), +(68850,28,-6686.64,-2162.67,244.195,NULL,0,150000,0,0,0,100,0); From 1d271a332654aaa54e5ee518bc77270a48dbf8c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 20:42:38 +0000 Subject: [PATCH 23/69] chore(DB): import pending files Referenced commit(s): e462aef6b09cefeb6c2ba1a14542a0595da18ddf --- .../rev_1784574123694351600.sql => db_world/2026_07_25_07.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784574123694351600.sql => db_world/2026_07_25_07.sql} (98%) diff --git a/data/sql/updates/pending_db_world/rev_1784574123694351600.sql b/data/sql/updates/db_world/2026_07_25_07.sql similarity index 98% rename from data/sql/updates/pending_db_world/rev_1784574123694351600.sql rename to data/sql/updates/db_world/2026_07_25_07.sql index a153eec12..e340f396f 100644 --- a/data/sql/updates/pending_db_world/rev_1784574123694351600.sql +++ b/data/sql/updates/db_world/2026_07_25_07.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_06 -> 2026_07_25_07 -- DELETE FROM `creature_formations` WHERE `memberGUID` IN (6877,6880,6883,6885,6886); INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES From afc33e7b285efd717dcd4ce75b590d99f2bcbdf6 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:10:15 -0300 Subject: [PATCH 24/69] fix(Core/Pools): respawn nested pools when the same child pool is re-selected (#26791) Co-authored-by: Gildor --- src/server/game/Pools/PoolMgr.cpp | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/server/game/Pools/PoolMgr.cpp b/src/server/game/Pools/PoolMgr.cpp index 0a22e2e9d..98cf90f5a 100644 --- a/src/server/game/Pools/PoolMgr.cpp +++ b/src/server/game/Pools/PoolMgr.cpp @@ -526,28 +526,14 @@ void PoolGroup::SpawnObject(ActivePoolData& spawns, uint32 limit, uint32 sPoolMgr->SaveQuestsToDB(false, false, true); } -// Method that does the respawn job on the specified creature -template <> -void PoolGroup::ReSpawn1Object(PoolObject* obj) +// Method that does the respawn job on the specified object +template +void PoolGroup::ReSpawn1Object(PoolObject* obj) { Despawn1Object(obj->guid); Spawn1Object(obj); } -// Method that does the respawn job on the specified gameobject -template <> -void PoolGroup::ReSpawn1Object(PoolObject* obj) -{ - Despawn1Object(obj->guid); - Spawn1Object(obj); -} - -// Nothing to do for a child Pool -template <> -void PoolGroup::ReSpawn1Object(PoolObject* /*obj*/) -{ -} - // Nothing to do for a quest template <> void PoolGroup::ReSpawn1Object(PoolObject* /*obj*/) From ccd6e1ffeff625e4e43bb7c561777ba9a6bf2e6f Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:58:49 -0300 Subject: [PATCH 25/69] fix(Scripts/BlackTemple): restore Supremus Molten Flame chase (#26806) --- .../Outland/BlackTemple/boss_supremus.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp b/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp index a81ab01a9..6566f8ec8 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp @@ -186,27 +186,24 @@ struct npc_supremus_punch_invisible_stalker : public ScriptedAI { npc_supremus_punch_invisible_stalker(Creature* creature) : ScriptedAI(creature) { } - void IsSummonedBy(WorldObject* /*summoner*/) override + void IsSummonedBy(WorldObject* summoner) override { - me->SetInCombatWithZone(); - if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 100.0f, true)) - me->AddThreat(target, 10000.f); - DoCastSelf(SPELL_MOLTEN_FLAME, true); + // Trigger creatures cannot have their own threat list, pick the chase target from the summoner's + if (Creature* supremus = summoner->ToCreature()) + if (Unit* target = supremus->AI()->SelectTarget(SelectTargetMethod::Random, 0, 100.0f, true)) + me->GetMotionMaster()->MoveFollow(target, 0.0f, 0.0f); + scheduler.Schedule(6s, 10s, [this](TaskContext /*context*/) { - me->CombatStop(); - me->SetReactState(REACT_PASSIVE); + me->GetMotionMaster()->MoveIdle(); }); } void UpdateAI(uint32 diff) override { scheduler.Update(diff); - - if (!UpdateVictim()) - return; } }; From 584637dc0f77b0efe50ea6fcbf6c7b82cb982e49 Mon Sep 17 00:00:00 2001 From: Joe Amenta <3792815+airbreather@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:57:50 -0400 Subject: [PATCH 26/69] fix(deps/jemalloc): use throw std::bad_alloc() instead of libstdc++ internal (#26438) Co-authored-by: Nefertumm --- deps/jemalloc/Hotfix throw bad_alloc.diff | 14 ++++++++++++++ deps/jemalloc/src/jemalloc_cpp.cpp | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 deps/jemalloc/Hotfix throw bad_alloc.diff diff --git a/deps/jemalloc/Hotfix throw bad_alloc.diff b/deps/jemalloc/Hotfix throw bad_alloc.diff new file mode 100644 index 000000000..89d1b7d03 --- /dev/null +++ b/deps/jemalloc/Hotfix throw bad_alloc.diff @@ -0,0 +1,14 @@ +# we changed this because jemalloc/jemalloc#2900 isn't in a tagged release yet +diff --git a/deps/jemalloc/src/jemalloc_cpp.cpp b/deps/jemalloc/src/jemalloc_cpp.cpp +index da0441a7c..05d5c3a7c 100644 +--- a/deps/jemalloc/src/jemalloc_cpp.cpp ++++ b/deps/jemalloc/src/jemalloc_cpp.cpp +@@ -67,7 +67,7 @@ handleOOM(std::size_t size, bool nothrow) { + } + + if (ptr == nullptr && !nothrow) +- std::__throw_bad_alloc(); ++ throw std::bad_alloc(); + return ptr; + } + diff --git a/deps/jemalloc/src/jemalloc_cpp.cpp b/deps/jemalloc/src/jemalloc_cpp.cpp index da0441a7c..05d5c3a7c 100644 --- a/deps/jemalloc/src/jemalloc_cpp.cpp +++ b/deps/jemalloc/src/jemalloc_cpp.cpp @@ -67,7 +67,7 @@ handleOOM(std::size_t size, bool nothrow) { } if (ptr == nullptr && !nothrow) - std::__throw_bad_alloc(); + throw std::bad_alloc(); return ptr; } From 891b9671ebd1b24532637255ae700e2306cc9acf Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:13:52 -0300 Subject: [PATCH 27/69] fix(Scripts/BlackTemple): fix crash on Shade of Akama evade (#26813) --- .../BlackTemple/boss_shade_of_akama.cpp | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/server/scripts/Outland/BlackTemple/boss_shade_of_akama.cpp b/src/server/scripts/Outland/BlackTemple/boss_shade_of_akama.cpp index f339915d0..7f43d69ff 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_shade_of_akama.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_shade_of_akama.cpp @@ -16,6 +16,7 @@ */ #include "CreatureScript.h" +#include "ObjectAccessor.h" #include "ScriptedCreature.h" #include "SpellScriptLoader.h" #include "black_temple.h" @@ -98,8 +99,8 @@ struct boss_shade_of_akama : public BossAI { boss_shade_of_akama(Creature* creature) : BossAI(creature, DATA_SHADE_OF_AKAMA) { } - std::list channelers; - std::list generators; + GuidVector channelers; + GuidVector generators; void Reset() override { @@ -114,11 +115,13 @@ struct boss_shade_of_akama : public BossAI void EnterEvadeMode(EvadeReason why) override { - for (Creature* generator : generators) - generator->AI()->DoAction(ACTION_GENERATOR_DESPAWN_ALL); + for (ObjectGuid const& generatorGuid : generators) + if (Creature* generator = ObjectAccessor::GetCreature(*me, generatorGuid)) + generator->AI()->DoAction(ACTION_GENERATOR_DESPAWN_ALL); - for (Creature* channeler : channelers) - channeler->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + for (ObjectGuid const& channelerGuid : channelers) + if (Creature* channeler = ObjectAccessor::GetCreature(*me, channelerGuid)) + channeler->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); BossAI::EnterEvadeMode(why); } @@ -128,8 +131,9 @@ struct boss_shade_of_akama : public BossAI BossAI::JustDied(killer); me->CastSpell(me, SPELL_SHADE_OF_AKAMA_TRIGGER, true); - for (Creature* generator : generators) - generator->AI()->DoAction(ACTION_GENERATOR_DESPAWN_ALL); + for (ObjectGuid const& generatorGuid : generators) + if (Creature* generator = ObjectAccessor::GetCreature(*me, generatorGuid)) + generator->AI()->DoAction(ACTION_GENERATOR_DESPAWN_ALL); if (Creature* akama = instance->GetCreature(DATA_AKAMA_SHADE)) akama->AI()->DoAction(ACTION_AKAMA_START_OUTRO); @@ -141,14 +145,22 @@ struct boss_shade_of_akama : public BossAI { instance->SetBossState(DATA_SHADE_OF_AKAMA, IN_PROGRESS); - me->GetCreatureListWithEntryInGrid(channelers, NPC_ASHTONGUE_CHANNELER, 40.0f); - me->GetCreatureListWithEntryInGrid(generators, NPC_CREATURE_GENERATOR_AKAMA, 100.0f); + std::list channelerList; + std::list generatorList; + me->GetCreatureListWithEntryInGrid(channelerList, NPC_ASHTONGUE_CHANNELER, 40.0f); + me->GetCreatureListWithEntryInGrid(generatorList, NPC_CREATURE_GENERATOR_AKAMA, 100.0f); - for (Creature* channeler : channelers) + for (Creature* channeler : channelerList) + { + channelers.push_back(channeler->GetGUID()); channeler->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + } - for (Creature* generator : generators) + for (Creature* generator : generatorList) + { + generators.push_back(generator->GetGUID()); generator->AI()->DoAction(ACTION_GENERATOR_START); + } ScheduleTimedEvent(1200ms, [&] { @@ -169,8 +181,9 @@ struct boss_shade_of_akama : public BossAI me->RemoveAurasDueToSpell(SPELL_AKAMA_SOUL_CHANNEL); scheduler.CancelAll(); - for (Creature* generator : generators) - generator->AI()->DoAction(ACTION_GENERATOR_STOP); + for (ObjectGuid const& generatorGuid : generators) + if (Creature* generator = ObjectAccessor::GetCreature(*me, generatorGuid)) + generator->AI()->DoAction(ACTION_GENERATOR_STOP); if (Creature* akama = instance->GetCreature(DATA_AKAMA_SHADE)) { @@ -222,7 +235,6 @@ struct npc_akama_shade : public ScriptedAI _sayLowHealth = false; _died = false; scheduler.CancelAll(); - _generators.clear(); } void MovementInform(uint32 type, uint32 point) override @@ -280,8 +292,9 @@ struct npc_akama_shade : public ScriptedAI else if (damage >= me->GetHealth() && !_died) { _died = true; - me->GetCreatureListWithEntryInGrid(_generators, NPC_CREATURE_GENERATOR_AKAMA, 100.0f); - for (Creature* generator : _generators) + std::list generators; + me->GetCreatureListWithEntryInGrid(generators, NPC_CREATURE_GENERATOR_AKAMA, 100.0f); + for (Creature* generator : generators) generator->AI()->DoAction(ACTION_GENERATOR_DESPAWN_ALL); damage = me->GetHealth() - 1; @@ -347,7 +360,6 @@ struct npc_akama_shade : public ScriptedAI private: bool _sayLowHealth; bool _died; - std::list _generators; }; struct npc_creature_generator_akama : public ScriptedAI From e0fe11ba46b885a01e4a4038001e0055822cc7ba Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:24:00 -0300 Subject: [PATCH 28/69] fix(Core/Movement): don't teleport unreleased corpses out of instances when falling under the map (#26768) --- src/server/game/Handlers/MovementHandler.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/server/game/Handlers/MovementHandler.cpp b/src/server/game/Handlers/MovementHandler.cpp index d809747c5..925281e39 100644 --- a/src/server/game/Handlers/MovementHandler.cpp +++ b/src/server/game/Handlers/MovementHandler.cpp @@ -22,7 +22,6 @@ #include "CellImpl.h" #include "Chat.h" #include "Corpse.h" -#include "GameGraveyard.h" #include "GameTime.h" #include "InstanceSaveMgr.h" #include "Log.h" @@ -520,15 +519,10 @@ void WorldSession::HandleMoverRelocation(MovementInfo& movementInfo, Unit* mover if (plrMover->IsAlive()) plrMover->KillPlayer(); } - else if (!plrMover->HasPlayerFlag(PLAYER_FLAGS_IS_OUT_OF_BOUNDS)) - { - GraveyardStruct const* grave = sGraveyard->GetClosestGraveyard(plrMover, plrMover->GetTeamId()); - if (grave) - { - plrMover->TeleportTo(grave->Map, grave->x, grave->y, grave->z, plrMover->GetOrientation()); - plrMover->Relocate(grave->x, grave->y, grave->z, plrMover->GetOrientation()); - } - } + // Rescue only released ghosts: teleporting an unreleased body would move the corpse + // out of instances (e.g. Eye of Eternity platform destruction, issue #25757). + else if (plrMover->HasPlayerFlag(PLAYER_FLAGS_GHOST) && !plrMover->HasPlayerFlag(PLAYER_FLAGS_IS_OUT_OF_BOUNDS)) + plrMover->RepopAtGraveyard(); } } } From 68e2b43b69c0a47a37268372c27a26f8f95de839 Mon Sep 17 00:00:00 2001 From: Vox Date: Sun, 26 Jul 2026 03:41:49 -0400 Subject: [PATCH 29/69] fix(DB/Creature): Prevent Vaelen the Flayed from attacking (#26766) --- .../pending_db_world/rev_1784856875186595900.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1784856875186595900.sql diff --git a/data/sql/updates/pending_db_world/rev_1784856875186595900.sql b/data/sql/updates/pending_db_world/rev_1784856875186595900.sql new file mode 100644 index 000000000..e4a35dc34 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1784856875186595900.sql @@ -0,0 +1,10 @@ +-- -------------------------------------------------------------------------------------------- +-- Icecrown (Northrend, map 571) +-- Vaelen the Flayed (Entry 30056, GUID 122535) +-- Prevent NPC from attacking other NPCs while chained up +-- ------------------------------------------- +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 30056; + +DELETE FROM `smart_scripts` WHERE (`entryorguid` = 30056) AND (`source_type` = 0) AND (`id` IN (1)); +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 +(30056, 0, 1, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Vaelen the Flayed - On Respawn - Set Reactstate Passive'); From 3899eedb10607b7ff1697ec865063ea812e6436a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jul 2026 07:43:01 +0000 Subject: [PATCH 30/69] chore(DB): import pending files Referenced commit(s): 68e2b43b69c0a47a37268372c27a26f8f95de839 --- .../rev_1784856875186595900.sql => db_world/2026_07_26_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1784856875186595900.sql => db_world/2026_07_26_00.sql} (96%) diff --git a/data/sql/updates/pending_db_world/rev_1784856875186595900.sql b/data/sql/updates/db_world/2026_07_26_00.sql similarity index 96% rename from data/sql/updates/pending_db_world/rev_1784856875186595900.sql rename to data/sql/updates/db_world/2026_07_26_00.sql index e4a35dc34..c373e391b 100644 --- a/data/sql/updates/pending_db_world/rev_1784856875186595900.sql +++ b/data/sql/updates/db_world/2026_07_26_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_25_07 -> 2026_07_26_00 -- -------------------------------------------------------------------------------------------- -- Icecrown (Northrend, map 571) -- Vaelen the Flayed (Entry 30056, GUID 122535) From 144f77eb2a575b8a2b57cccd15ba297dd3f84ae8 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:16:02 -0300 Subject: [PATCH 31/69] fix(DB/Immunities): allow Mind Control on 25-man Naxxramas Worshippers (#26815) --- .../updates/pending_db_world/rev_1785029398836777100.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785029398836777100.sql diff --git a/data/sql/updates/pending_db_world/rev_1785029398836777100.sql b/data/sql/updates/pending_db_world/rev_1785029398836777100.sql new file mode 100644 index 000000000..cadc894f4 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785029398836777100.sql @@ -0,0 +1,6 @@ +-- On 25-man, Faerlina's Frenzy is countered by Mind Controlling a Naxxramas +-- Worshipper to cast Widow's Embrace, so the 25-man entry must not be +-- charm-immune: -414 (with CHARM) -> -413 (same set without CHARM). +-- The 10-man entry (16506) keeps -414: there the Worshipper cannot be MC'd +-- and is killed next to Faerlina instead (casts Widow's Embrace on death). +UPDATE `creature_template` SET `CreatureImmunitiesId` = -413 WHERE `entry` = 29274; From 5d87047721074f9f4f370ae4dc9b967f717ccb0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jul 2026 09:16:57 +0000 Subject: [PATCH 32/69] chore(DB): import pending files Referenced commit(s): 144f77eb2a575b8a2b57cccd15ba297dd3f84ae8 --- .../rev_1785029398836777100.sql => db_world/2026_07_26_01.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785029398836777100.sql => db_world/2026_07_26_01.sql} (91%) diff --git a/data/sql/updates/pending_db_world/rev_1785029398836777100.sql b/data/sql/updates/db_world/2026_07_26_01.sql similarity index 91% rename from data/sql/updates/pending_db_world/rev_1785029398836777100.sql rename to data/sql/updates/db_world/2026_07_26_01.sql index cadc894f4..6303da3e3 100644 --- a/data/sql/updates/pending_db_world/rev_1785029398836777100.sql +++ b/data/sql/updates/db_world/2026_07_26_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_26_00 -> 2026_07_26_01 -- On 25-man, Faerlina's Frenzy is countered by Mind Controlling a Naxxramas -- Worshipper to cast Widow's Embrace, so the 25-man entry must not be -- charm-immune: -414 (with CHARM) -> -413 (same set without CHARM). From d1e17c19e4a0006c163b1d480677fe8dfadcb571 Mon Sep 17 00:00:00 2001 From: sudlud Date: Sun, 26 Jul 2026 11:31:12 +0200 Subject: [PATCH 33/69] =?UTF-8?q?Revert=20"feat(CI):=20skip=20pch/nopch=20?= =?UTF-8?q?core=20builds=20on=20DB-only=20pull=20requests=E2=80=A6=20(#268?= =?UTF-8?q?23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/core-build-nopch.yml | 6 ------ .github/workflows/core-build-pch.yml | 6 ------ 2 files changed, 12 deletions(-) diff --git a/.github/workflows/core-build-nopch.yml b/.github/workflows/core-build-nopch.yml index a55e1a415..972e30c24 100644 --- a/.github/workflows/core-build-nopch.yml +++ b/.github/workflows/core-build-nopch.yml @@ -8,12 +8,6 @@ on: - opened - reopened - synchronize - # A PR that only touches data/ (SQL) leaves the C++ untouched, so there is - # nothing to compile. Dashboard CI runs on every PR unfiltered and already - # imports the SQL and starts a real worldserver, so coverage is unaffected. - # Pushes to master are not filtered and always build. - paths-ignore: - - 'data/**' concurrency: # One concurrency group per workflow + ref. diff --git a/.github/workflows/core-build-pch.yml b/.github/workflows/core-build-pch.yml index 242473e78..3fd3aa0c1 100644 --- a/.github/workflows/core-build-pch.yml +++ b/.github/workflows/core-build-pch.yml @@ -8,12 +8,6 @@ on: - opened - reopened - synchronize - # A PR that only touches data/ (SQL) leaves the C++ untouched, so there is - # nothing to compile. Dashboard CI runs on every PR unfiltered and already - # imports the SQL and starts a real worldserver, so coverage is unaffected. - # Pushes to master are not filtered and always build. - paths-ignore: - - 'data/**' concurrency: # One concurrency group per workflow + ref. From c9f384745e83ea4f32c843a1d42fe671a8540928 Mon Sep 17 00:00:00 2001 From: sogladev Date: Sun, 26 Jul 2026 11:38:27 +0200 Subject: [PATCH 34/69] =?UTF-8?q?fix(DB/Immunities):=20Embalming=20Slime,?= =?UTF-8?q?=20Patchwork=20Golem,=20Bile=20Retcher,=20S=E2=80=A6=20(#26822)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../updates/pending_db_world/rev_1785048034059118523.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785048034059118523.sql diff --git a/data/sql/updates/pending_db_world/rev_1785048034059118523.sql b/data/sql/updates/pending_db_world/rev_1785048034059118523.sql new file mode 100644 index 000000000..93e4cc559 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785048034059118523.sql @@ -0,0 +1,6 @@ +-- +-- Embalming Slime +UPDATE `creature_template` SET `CreatureImmunitiesId` = -93 WHERE `entry` = 29355; +-- Patchwork Golem, Bile Retcher, and Sludge Belcher +UPDATE `creature_template` SET `CreatureImmunitiesId` = -405 WHERE `entry` IN (16017, 16018, 29347, 29353); +UPDATE `creature_template` SET `CreatureImmunitiesId` = -93 WHERE `entry` IN (16029, 29356); From 1c3222eec40abea0a5199dc90ba7a8a8c880fccd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jul 2026 09:39:39 +0000 Subject: [PATCH 35/69] chore(DB): import pending files Referenced commit(s): c9f384745e83ea4f32c843a1d42fe671a8540928 --- .../rev_1785048034059118523.sql => db_world/2026_07_26_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785048034059118523.sql => db_world/2026_07_26_02.sql} (89%) diff --git a/data/sql/updates/pending_db_world/rev_1785048034059118523.sql b/data/sql/updates/db_world/2026_07_26_02.sql similarity index 89% rename from data/sql/updates/pending_db_world/rev_1785048034059118523.sql rename to data/sql/updates/db_world/2026_07_26_02.sql index 93e4cc559..5c4b18535 100644 --- a/data/sql/updates/pending_db_world/rev_1785048034059118523.sql +++ b/data/sql/updates/db_world/2026_07_26_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_26_01 -> 2026_07_26_02 -- -- Embalming Slime UPDATE `creature_template` SET `CreatureImmunitiesId` = -93 WHERE `entry` = 29355; From 70da2f25d67cebb0670a84ccf685abb923ca4549 Mon Sep 17 00:00:00 2001 From: Rocco Silipo <108557877+Rorschach91@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:49:24 +0200 Subject: [PATCH 36/69] refactor(Scripts/ScarletEnclave): Consolidate chapter scripts into a single file. (#26804) --- .../ScarletEnclave/chapter1.cpp | 477 --- .../ScarletEnclave/chapter2.cpp | 1085 ------ .../ScarletEnclave/chapter3.cpp | 91 - .../ScarletEnclave/chapter5.cpp | 1317 -------- .../zone_the_scarlet_enclave.cpp | 143 - .../eastern_kingdoms_script_loader.cpp | 8 - .../zone_the_scarlet_enclave.cpp | 2945 +++++++++++++++++ 7 files changed, 2945 insertions(+), 3121 deletions(-) delete mode 100644 src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp delete mode 100644 src/server/scripts/EasternKingdoms/ScarletEnclave/chapter2.cpp delete mode 100644 src/server/scripts/EasternKingdoms/ScarletEnclave/chapter3.cpp delete mode 100644 src/server/scripts/EasternKingdoms/ScarletEnclave/chapter5.cpp delete mode 100644 src/server/scripts/EasternKingdoms/ScarletEnclave/zone_the_scarlet_enclave.cpp create mode 100644 src/server/scripts/EasternKingdoms/zone_the_scarlet_enclave.cpp diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp deleted file mode 100644 index d22350896..000000000 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp +++ /dev/null @@ -1,477 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#include "CombatAI.h" -#include "CreatureScript.h" -#include "CreatureTextMgr.h" -#include "GameObjectScript.h" -#include "MoveSplineInit.h" -#include "ObjectMgr.h" -#include "PassiveAI.h" -#include "Player.h" -#include "ScriptedCreature.h" -#include "ScriptedEscortAI.h" -#include "ScriptedGossip.h" -#include "SpellInfo.h" -#include "SpellScript.h" -#include "SpellScriptLoader.h" - - /*###### - ## npc_eye_of_acherus - ######*/ - -enum EyeOfAcherusMisc -{ - SPELL_THE_EYE_OF_ACHERUS = 51852, -}; - -enum DeathComesFromOnHigh -{ - SPELL_CALL_OF_THE_DEAD = 51900 -}; - -enum GothikActions -{ - ACTION_DK_INITIATE_ASSAULT_ROAR = 15 -}; - -// 51904 - Summon Ghouls On Scarlet Crusade -class spell_q12641_death_comes_from_on_high_summon_ghouls : public SpellScript -{ - PrepareSpellScript(spell_q12641_death_comes_from_on_high_summon_ghouls); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_CALL_OF_THE_DEAD }); - } - - void HandleScriptEffect(SpellEffIndex effIndex) - { - PreventHitEffect(effIndex); - if (Unit* target = GetHitUnit()) - target->CastSpell(target, SPELL_CALL_OF_THE_DEAD, true); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_q12641_death_comes_from_on_high_summon_ghouls::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -// 52694 - Recall Eye of Acherus -class spell_q12641_death_comes_from_on_high_recall_eye : public SpellScript -{ - PrepareSpellScript(spell_q12641_death_comes_from_on_high_recall_eye); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_THE_EYE_OF_ACHERUS }); - } - - void HandleScriptEffect(SpellEffIndex effIndex) - { - PreventHitEffect(effIndex); - Unit* caster = GetCaster(); - Unit* owner = caster->GetCharmerOrOwner(); - - if (!caster || !owner) - return; - - if (owner->HasAura(SPELL_THE_EYE_OF_ACHERUS)) - owner->RemoveAurasDueToSpell(SPELL_THE_EYE_OF_ACHERUS); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_q12641_death_comes_from_on_high_recall_eye::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -// 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, - MAX_GHOULS = 5, - - SPELL_GHOUL_EMERGE = 50142, - SPELL_SUMMON_SCARLET_GHOST = 52505, - SPELL_GHOUL_SUBMERGE = 26234, - - EVENT_GHOUL_RESTORE_STATE = 1, - EVENT_GHOUL_CHECK_COMBAT = 2, - EVENT_GHOUL_EMOTE = 3, - EVENT_GHOUL_MOVE_TO_PIT = 4, - - SAY_GOTHIK_PIT = 0 -}; - -class spell_item_gift_of_the_harvester : public SpellScript -{ - PrepareSpellScript(spell_item_gift_of_the_harvester); - - SpellCastResult CheckRequirement() - { - std::list ghouls; - GetCaster()->GetAllMinionsByEntry(ghouls, NPC_GHOUL); - if (ghouls.size() >= MAX_GHOULS) - { - SetCustomCastResultMessage(SPELL_CUSTOM_ERROR_TOO_MANY_GHOULS); - return SPELL_FAILED_CUSTOM_ERROR; - } - - return SPELL_CAST_OK; - } - - void Register() override - { - OnCheckCast += SpellCheckCastFn(spell_item_gift_of_the_harvester::CheckRequirement); - } -}; - -class spell_q12698_the_gift_that_keeps_on_giving : public SpellScript -{ - PrepareSpellScript(spell_q12698_the_gift_that_keeps_on_giving); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_SUMMON_SCARLET_GHOST }); - } - - void HandleScriptEffect(SpellEffIndex /*effIndex*/) - { - if (GetOriginalCaster() && GetHitUnit()) - GetOriginalCaster()->CastSpell(GetHitUnit(), urand(0, 1) ? GetEffectValue() : SPELL_SUMMON_SCARLET_GHOST, true); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_q12698_the_gift_that_keeps_on_giving::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -class npc_scarlet_ghoul : public CreatureScript -{ -public: - npc_scarlet_ghoul() : CreatureScript("npc_scarlet_ghoul") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_scarlet_ghoulAI(creature); - } - - struct npc_scarlet_ghoulAI : public ScriptedAI - { - npc_scarlet_ghoulAI(Creature* creature) : ScriptedAI(creature) - { - } - - EventMap events; - ObjectGuid gothikGUID; - - void InitializeAI() override - { - me->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE); - ScriptedAI::InitializeAI(); - me->SetReactState(REACT_PASSIVE); - - events.ScheduleEvent(EVENT_GHOUL_EMOTE, 1ms); - events.ScheduleEvent(EVENT_GHOUL_RESTORE_STATE, 3500ms); - } - - void OwnerAttackedBy(Unit* attacker) override - { - if (!me->IsInCombat() && me->GetReactState() == REACT_DEFENSIVE) - AttackStart(attacker); - } - - void SetGUID(ObjectGuid const& guid, int32) override - { - gothikGUID = guid; - events.ScheduleEvent(EVENT_GHOUL_MOVE_TO_PIT, 3s); - me->GetMotionMaster()->Clear(false); - } - - void MovementInform(uint32 type, uint32 point) override - { - if (type == POINT_MOTION_TYPE && point == 1) - { - me->DespawnOrUnsummon(1500ms); - me->CastSpell(me, SPELL_GHOUL_SUBMERGE, true); - } - } - - void UpdateAI(uint32 diff) override - { - events.Update(diff); - switch (events.ExecuteEvent()) - { - case EVENT_GHOUL_MOVE_TO_PIT: - me->GetMotionMaster()->MovePoint(1, 2364.77f, -5776.14f, 151.36f); - if (Creature* gothik = ObjectAccessor::GetCreature(*me, gothikGUID)) - gothik->AI()->DoAction(SAY_GOTHIK_PIT); - break; - case EVENT_GHOUL_EMOTE: - me->CastSpell(me, SPELL_GHOUL_EMERGE, true); - break; - case EVENT_GHOUL_RESTORE_STATE: - me->SetReactState(REACT_DEFENSIVE); - me->RemoveUnitFlag(UNIT_FLAG_DISABLE_MOVE); - if (Player* owner = me->GetCharmerOrOwnerPlayerOrPlayerItself()) - me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, frand(0.0f, 2 * M_PI)); - events.ScheduleEvent(EVENT_GHOUL_CHECK_COMBAT, 1s); - return; - case EVENT_GHOUL_CHECK_COMBAT: - if (!me->IsInCombat()) - if (Player* owner = me->GetCharmerOrOwnerPlayerOrPlayerItself()) - if (owner->GetVictim()) - AttackStart(owner->GetVictim()); - - events.Repeat(1s); - return; - } - - if (!UpdateVictim()) - return; - - DoMeleeAttackIfReady(); - } - }; -}; - -class npc_dkc1_gothik : public CreatureScript -{ -public: - npc_dkc1_gothik() : CreatureScript("npc_dkc1_gothik") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_dkc1_gothikAI(creature); - } - - struct npc_dkc1_gothikAI : public ScriptedAI - { - npc_dkc1_gothikAI(Creature* creature) : ScriptedAI(creature) { spoken = 0; } - - int32 spoken; - - void DoAction(int32 action) override - { - if (action == SAY_GOTHIK_PIT && spoken <= 0) - { - spoken = 5000; - Talk(SAY_GOTHIK_PIT); - } - - if (action == ACTION_DK_INITIATE_ASSAULT_ROAR) - me->HandleEmoteCommand(EMOTE_ONESHOT_ROAR); - } - - void MoveInLineOfSight(Unit* who) override - { - ScriptedAI::MoveInLineOfSight(who); - - if (!who->IsImmuneToNPC() && who->GetEntry() == NPC_GHOUL && me->IsWithinDistInMap(who, 10.0f)) - if (Unit* owner = who->GetOwner()) - if (Player* player = owner->ToPlayer()) - { - Creature* creature = who->ToCreature(); - if (player->GetQuestStatus(12698) == QUEST_STATUS_INCOMPLETE) - creature->CastSpell(owner, 52517, true); - - creature->AI()->SetGUID(me->GetGUID()); - creature->SetImmuneToAll(true); - } - } - - void UpdateAI(uint32 diff) override - { - if (spoken > 0) - spoken -= diff; - - ScriptedAI::UpdateAI(diff); - } - }; -}; - -/*###### -##Quest 12848 -######*/ - -#define GCD_CAST 1 - -enum UnworthyInitiate -{ - SPELL_DK_INITIATE_VISUAL = 51519, -}; - -class spell_death_knight_initiate_visual : public SpellScript -{ - PrepareSpellScript(spell_death_knight_initiate_visual); - - void HandleScriptEffect(SpellEffIndex /* effIndex */) - { - Creature* target = GetHitCreature(); - if (!target) - return; - - uint32 spellId; - switch (target->GetDisplayId()) - { - case 25369: spellId = 51552; break; // bloodelf female - case 25373: spellId = 51551; break; // bloodelf male - case 25363: spellId = 51542; break; // draenei female - case 25357: spellId = 51541; break; // draenei male - case 25361: spellId = 51537; break; // dwarf female - case 25356: spellId = 51538; break; // dwarf male - case 25372: spellId = 51550; break; // forsaken female - case 25367: spellId = 51549; break; // forsaken male - case 25362: spellId = 51540; break; // gnome female - case 25359: spellId = 51539; break; // gnome male - case 25355: spellId = 51534; break; // human female - case 25354: spellId = 51520; break; // human male - case 25360: spellId = 51536; break; // nightelf female - case 25358: spellId = 51535; break; // nightelf male - case 25368: spellId = 51544; break; // orc female - case 25364: spellId = 51543; break; // orc male - case 25371: spellId = 51548; break; // tauren female - case 25366: spellId = 51547; break; // tauren male - case 25370: spellId = 51545; break; // troll female - case 25365: spellId = 51546; break; // troll male - default: return; - } - - target->CastSpell(target, spellId, true); - target->LoadEquipment(); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_death_knight_initiate_visual::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -enum spells_lich_king_whisper -{ - SPELL_LICH_KING_VO_BLOCKER = 58207, - SPELL_LICHKINGDK001 = 58208, - SPELL_LICHKINGDK002 = 58209, - SPELL_LICHKINGDK003 = 58210, - SPELL_LICHKINGDK004 = 58211, - SPELL_LICHKINGDK005 = 58212, - SPELL_LICHKINGDK006 = 58213, - SPELL_LICHKINGDK007 = 58214, - SPELL_LICHKINGDK008 = 58215, - SPELL_LICHKINGDK009 = 58216, - SPELL_LICHKINGDK010 = 58217, - SPELL_LICHKINGDK011 = 58218, - SPELL_LICHKINGDK012 = 58219, - SPELL_LICHKINGDK013 = 58220, - SPELL_LICHKINGDK014 = 58221, - SPELL_LICHKINGDK015 = 58222, - SPELL_LICHKINGDK016 = 58223 -}; - -//spell 58207 rand Whisper -class spell_lich_king_vo_blocker : public AuraScript -{ - PrepareAuraScript(spell_lich_king_vo_blocker); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo - ({ - SPELL_LICHKINGDK001, SPELL_LICHKINGDK002, SPELL_LICHKINGDK003, SPELL_LICHKINGDK004, - SPELL_LICHKINGDK005, SPELL_LICHKINGDK006, SPELL_LICHKINGDK007, SPELL_LICHKINGDK008, - SPELL_LICHKINGDK009, SPELL_LICHKINGDK010, SPELL_LICHKINGDK011, SPELL_LICHKINGDK012, - SPELL_LICHKINGDK013, SPELL_LICHKINGDK014, SPELL_LICHKINGDK015, SPELL_LICHKINGDK016 - }); - } - - void HandleEffectApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) - { - if (Player* target = GetTarget()->ToPlayer()) - { - //spell 58208-58223 - GetCaster()->CastSpell(target, urand(SPELL_LICHKINGDK001, SPELL_LICHKINGDK016), true); - } - } - - void Register() override - { - OnEffectApply += AuraEffectApplyFn(spell_lich_king_vo_blocker::HandleEffectApply, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); - } -}; - -// 58208 - 58224 - Creature - The Lich King (28765) Whisper -class spell_lich_king_whisper : public SpellScript -{ - PrepareSpellScript(spell_lich_king_whisper); - - bool Validate(SpellInfo const* spellInfo) override - { - return sObjectMgr->GetBroadcastText(uint32(spellInfo->GetEffect(EFFECT_0).CalcValue())) && - sSoundEntriesStore.LookupEntry(uint32(spellInfo->GetEffect(EFFECT_1).CalcValue())); - } - - void HandleScript(SpellEffIndex /*effIndex*/) - { - if (Player* player = GetHitPlayer()) - GetCaster()->Whisper(uint32(GetEffectValue()), player, false); - } - - void HandleDummy(SpellEffIndex /*effIndex*/) - { - if (Player* player = GetHitPlayer()) - player->PlayDistanceSound(uint32(GetEffectValue()), player); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_lich_king_whisper::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - OnEffectHitTarget += SpellEffectFn(spell_lich_king_whisper::HandleDummy, EFFECT_1, SPELL_EFFECT_DUMMY); - } -}; - -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(); - new npc_dkc1_gothik(); - RegisterSpellScript(spell_death_knight_initiate_visual); - RegisterSpellScript(spell_lich_king_whisper); - RegisterSpellScript(spell_lich_king_vo_blocker); -} diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter2.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter2.cpp deleted file mode 100644 index 333c6ffee..000000000 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter2.cpp +++ /dev/null @@ -1,1085 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#include "CombatAI.h" -#include "CreatureScript.h" -#include "CreatureTextMgr.h" -#include "ScriptedGossip.h" -#include "Player.h" -#include "ScriptedCreature.h" -#include "ScriptedEscortAI.h" -#include "SpellInfo.h" -#include "SpellScript.h" -#include "MotionMaster.h" -#include "ObjectAccessor.h" -#include - -/*###### -## npc_koltira_deathweaver -######*/ - -enum Koltira -{ - SAY_BREAKOUT0 = 0, - SAY_BREAKOUT1 = 1, - SAY_BREAKOUT2 = 2, - SAY_BREAKOUT3 = 3, - SAY_BREAKOUT4 = 4, - SAY_BREAKOUT5 = 5, - SAY_BREAKOUT6 = 6, - SAY_BREAKOUT7 = 7, - SAY_BREAKOUT8 = 8, - SAY_BREAKOUT9 = 9, - SAY_BREAKOUT10 = 10, - EMOTE_KOLTIRA_COLLAPSES = 11, - - SAY_VALROTH_WAVE3 = 0, - SAY_VALROTH_AGGRO = 1, - SAY_VALROTH_WAVE1 = 4, - SAY_VALROTH_WAVE2 = 5, - - SPELL_KOLTIRA_TRANSFORM = 52899, - SPELL_ANTI_MAGIC_ZONE = 52894, - - QUEST_BREAKOUT = 12727, - - NPC_CRIMSON_ACOLYTE = 29007, - NPC_HIGH_INQUISITOR_VALROTH = 29001, - - //not sure about this id - //NPC_DEATH_KNIGHT_MOUNT = 29201, - MODEL_DEATH_KNIGHT_MOUNT = 25278, - - POINT_STAND_UP = 1, - POINT_BOX = 2, - POINT_ANTI_MAGIC_ZONE = 3, - - POINT_MOUNT = 1, - POINT_DESPAWN = 2 -}; - -class npc_koltira_deathweaver : public CreatureScript -{ -public: - npc_koltira_deathweaver() : CreatureScript("npc_koltira_deathweaver") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_koltira_deathweaverAI(creature); - } - - struct npc_koltira_deathweaverAI : public ScriptedAI - { - npc_koltira_deathweaverAI(Creature* creature) : ScriptedAI(creature) { } - - void Reset() override - { - scheduler.CancelAll(); - me->m_Events.KillAllEvents(false); - me->SetUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); - me->setActive(false); - SetEquipmentSlots(true); - } - - void StartEvent() - { - if (!me->HasNpcFlag(UNIT_NPC_FLAG_GOSSIP)) // Already in progress - return; - - me->SetStandState(UNIT_STAND_STATE_SIT); - me->RemoveNpcFlag(UNIT_NPC_FLAG_GOSSIP); - me->setActive(true); - - Talk(SAY_BREAKOUT0); - - me->m_Events.AddEventAtOffset([&] { - me->GetMotionMaster()->MoveWaypoint(me->GetEntry() * 10, false); - }, 5s); - } - - void sQuestAccept(Player* /*player*/, Quest const* quest) override - { - if (quest->GetQuestId() == QUEST_BREAKOUT) - StartEvent(); - } - - void sGossipSelect(Player* player, uint32 /*menuId*/, uint32 /*gossipListId*/) override - { - if (player->GetQuestStatus(QUEST_BREAKOUT) == QUEST_STATUS_INCOMPLETE) - { - CloseGossipMenuFor(player); - StartEvent(); - } - } - - void MovementInform(uint32 type, uint32 id) override - { - if (type != WAYPOINT_MOTION_TYPE) - return; - - if (!me->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC)) - { - if (id == POINT_MOUNT) - me->Mount(MODEL_DEATH_KNIGHT_MOUNT); - else if (id == POINT_DESPAWN) - { - me->Dismount(); - me->DespawnOrUnsummon(); - } - - return; - } - - switch (id) - { - case POINT_STAND_UP: - Talk(SAY_BREAKOUT1); - break; - case POINT_BOX: - me->SetStandState(UNIT_STAND_STATE_KNEEL); - - scheduler.Schedule(5s, [this](TaskContext context) - { - switch (context.GetRepeatCounter()) - { - case 0: - Talk(SAY_BREAKOUT3); - - // Shouldn't actually be spawned at this point, but no way to send his yells otherwise? - if (Creature* valroth = me->SummonCreature(NPC_HIGH_INQUISITOR_VALROTH, 1640.8596f, -6030.834f, 134.82211f, 4.606426715850830078f, TEMPSUMMON_MANUAL_DESPAWN)) - { - _valrothGUID = valroth->GetGUID(); - valroth->AI()->Talk(SAY_VALROTH_WAVE1); - valroth->SetReactState(REACT_PASSIVE); - } - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.6724f, -6032.0527f, 134.82213f, 4.654973506927490234f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint(NPC_CRIMSON_ACOLYTE * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.0055f, -6031.893f, 134.82211f, 0.401425719261169433f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 1) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1639.7053f, -6031.7373f, 134.82213f, 2.443460941314697265f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 2) * 10, false); - break; - case 1: - Talk(SAY_BREAKOUT4); - - if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) - valroth->AI()->Talk(SAY_VALROTH_WAVE2); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.7958f, -6030.307f, 134.82211f, 4.65355682373046875f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 3) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.7305f, -6030.751f, 134.82211f, 6.143558979034423828f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 4) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1639.4657f, -6030.404f, 134.82211f, 4.502949237823486328f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 5) * 10, false); - break; - case 2: - Talk(SAY_BREAKOUT5); - - if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) - valroth->AI()->Talk(SAY_VALROTH_WAVE3); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.3405f, -6031.436f, 134.82211f, 4.612849712371826171f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 6) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1642.0404f, -6030.3843f, 134.82211f, 1.378810048103332519f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 7) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.1162f, -6029.7817f, 134.82211f, 5.707226753234863281f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 8) * 10, false); - - if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.9948f, -6029.8027f, 134.82211f, 1.605702877044677734f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) - acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 9) * 10, false); - break; - case 3: - Talk(SAY_BREAKOUT6); - me->m_Events.AddEventAtOffset([this] - { - Talk(EMOTE_KOLTIRA_COLLAPSES, me); - me->KillSelf(); - - if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) - valroth->DespawnOrUnsummon(); - }, 2min); - - if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) - { - valroth->AI()->Talk(SAY_VALROTH_AGGRO); - valroth->SetReactState(REACT_AGGRESSIVE); - valroth->GetMotionMaster()->MoveWaypoint(NPC_HIGH_INQUISITOR_VALROTH * 10, false); - } - return; - default: - break; - } - - context.Repeat(20s); - }); - - scheduler.Schedule(3s, [this](TaskContext) - { - DoCastSelf(SPELL_KOLTIRA_TRANSFORM); - me->LoadEquipment(); - }); - break; - case POINT_ANTI_MAGIC_ZONE: - me->SetStandState(UNIT_STAND_STATE_KNEEL); - Talk(SAY_BREAKOUT2); - DoCastSelf(SPELL_ANTI_MAGIC_ZONE); - break; - default: - break; - } - } - - void SummonedCreatureDies(Creature* summon, Unit*) override - { - if (summon->GetEntry() == NPC_HIGH_INQUISITOR_VALROTH) - { - me->m_Events.KillAllEvents(false); - me->RemoveAurasDueToSpell(SPELL_ANTI_MAGIC_ZONE); - me->SetStandState(UNIT_STAND_STATE_STAND); - Talk(SAY_BREAKOUT8, 3s); - Talk(SAY_BREAKOUT9, 8s); - scheduler.Schedule(11s, [this](TaskContext) - { - Talk(SAY_BREAKOUT10); - SetInvincibility(true); - me->SetReactState(REACT_PASSIVE); - me->RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); - me->GetMotionMaster()->MoveWaypoint((me->GetEntry() + 1) * 10, false); - }); - } - } - - void UpdateAI(uint32 diff) override - { - scheduler.Update(diff); - } - - private: - ObjectGuid _valrothGUID; - }; -}; - -//Scarlet courier -enum ScarletCourierEnum -{ - SAY_TREE1 = 0, - SAY_TREE2 = 1, - SPELL_SHOOT = 52818, - GO_INCONSPICUOUS_TREE = 191144, - NPC_SCARLET_COURIER = 29076 -}; - -class npc_scarlet_courier : public CreatureScript -{ -public: - npc_scarlet_courier() : CreatureScript("npc_scarlet_courier") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_scarlet_courierAI(creature); - } - - struct npc_scarlet_courierAI : public ScriptedAI - { - npc_scarlet_courierAI(Creature* creature) : ScriptedAI(creature) { } - - uint32 uiStage; - uint32 uiStage_timer; - - void Reset() override - { - me->Mount(14338); // not sure about this id - uiStage = 1; - uiStage_timer = 3000; - } - - void JustEngagedWith(Unit* /*who*/) override - { - Talk(SAY_TREE2); - me->Dismount(); - uiStage = 0; - } - - void MovementInform(uint32 type, uint32 id) override - { - if (type != POINT_MOTION_TYPE) - return; - - if (id == 1) - uiStage = 2; - } - - void UpdateAI(uint32 diff) override - { - if (uiStage && !me->IsInCombat()) - { - if (uiStage_timer <= diff) - { - switch (uiStage) - { - case 1: - me->SetWalk(true); - if (GameObject* tree = me->FindNearestGameObject(GO_INCONSPICUOUS_TREE, 40.0f)) - { - Talk(SAY_TREE1); - float x, y, z; - tree->GetContactPoint(me, x, y, z); - me->GetMotionMaster()->MovePoint(1, x, y, z); - } - break; - case 2: - if (GameObject* tree = me->FindNearestGameObject(GO_INCONSPICUOUS_TREE, 40.0f)) - if (Unit* unit = tree->GetOwner()) - AttackStart(unit); - break; - } - uiStage_timer = 3000; - uiStage = 0; - } - else uiStage_timer -= diff; - } - - if (!UpdateVictim()) - return; - - DoMeleeAttackIfReady(); - } - }; -}; - -/*###### -## npc_a_special_surprise -######*/ -//used by 29032, 29061, 29065, 29067, 29068, 29070, 29074, 29072, 29073, 29071 but signed for 29032 -enum SpecialSurprise -{ - SAY_EXEC_START = 0, - SAY_EXEC_PROG = 1, - SAY_EXEC_NAME = 2, - SAY_EXEC_RECOG = 3, - SAY_EXEC_NOREM = 4, - SAY_EXEC_THINK = 5, - SAY_EXEC_LISTEN = 6, - SAY_EXEC_TIME = 7, - SAY_EXEC_WAITING = 8, - EMOTE_DIES = 9, - - SAY_PLAGUEFIST = 0, - NPC_PLAGUEFIST = 29053 -}; - -class npc_a_special_surprise : public CreatureScript -{ -public: - npc_a_special_surprise() : CreatureScript("npc_a_special_surprise") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_a_special_surpriseAI(creature); - } - - struct npc_a_special_surpriseAI : public ScriptedAI - { - npc_a_special_surpriseAI(Creature* creature) : ScriptedAI(creature) { } - - uint32 ExecuteSpeech_Timer; - uint32 ExecuteSpeech_Counter; - ObjectGuid PlayerGUID; - - void Reset() override - { - ExecuteSpeech_Timer = 0; - ExecuteSpeech_Counter = 0; - PlayerGUID.Clear(); - - me->SetReactState(REACT_PASSIVE); - me->SetImmuneToPC(true); - } - - bool MeetQuestCondition(Player* player) - { - switch (me->GetEntry()) - { - case 29061: // Ellen Stanbridge - if (player->GetQuestStatus(12742) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29072: // Kug Ironjaw - if (player->GetQuestStatus(12748) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29067: // Donovan Pulfrost - if (player->GetQuestStatus(12744) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29065: // Yazmina Oakenthorn - if (player->GetQuestStatus(12743) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29071: // Antoine Brack - if (player->GetQuestStatus(12750) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29032: // Malar Bravehorn - if (player->GetQuestStatus(12739) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29068: // Goby Blastenheimer - if (player->GetQuestStatus(12745) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29073: // Iggy Darktusk - if (player->GetQuestStatus(12749) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29074: // Lady Eonys - if (player->GetQuestStatus(12747) == QUEST_STATUS_INCOMPLETE) - return true; - break; - case 29070: // Valok the Righteous - if (player->GetQuestStatus(12746) == QUEST_STATUS_INCOMPLETE) - return true; - break; - } - - return false; - } - - void MoveInLineOfSight(Unit* who) override - - { - if (PlayerGUID || !who->IsPlayer() || !who->IsWithinDist(me, INTERACTION_DISTANCE)) - return; - - if (MeetQuestCondition(who->ToPlayer())) - PlayerGUID = who->GetGUID(); - } - - void UpdateAI(uint32 diff) override - { - if (PlayerGUID && !me->GetVictim() && me->IsAlive()) - { - if (ExecuteSpeech_Timer <= diff) - { - Player* player = ObjectAccessor::GetPlayer(*me, PlayerGUID); - - if (!player) - { - Reset(); - return; - } - - switch (ExecuteSpeech_Counter) - { - case 0: - Talk(SAY_EXEC_START, player); - break; - case 1: - me->SetStandState(UNIT_STAND_STATE_STAND); - break; - case 2: - Talk(SAY_EXEC_PROG, player); - break; - case 3: - Talk(SAY_EXEC_NAME, player); - break; - case 4: - Talk(SAY_EXEC_RECOG, player); - break; - case 5: - Talk(SAY_EXEC_NOREM, player); - break; - case 6: - Talk(SAY_EXEC_THINK, player); - break; - case 7: - Talk(SAY_EXEC_LISTEN, player); - break; - case 8: - if (Creature* Plaguefist = GetClosestCreatureWithEntry(me, NPC_PLAGUEFIST, 85.0f)) - { - Plaguefist->AI()->Talk(SAY_PLAGUEFIST, player); - } - break; - case 9: - Talk(SAY_EXEC_TIME, player); - me->SetStandState(UNIT_STAND_STATE_KNEEL); - me->SetReactState(REACT_PASSIVE); - me->SetImmuneToPC(false); - break; - case 10: - Talk(SAY_EXEC_WAITING, player); - break; - case 11: - Talk(EMOTE_DIES); - me->setDeathState(DeathState::JustDied); - me->SetHealth(0); - return; - } - - if (ExecuteSpeech_Counter >= 9) - ExecuteSpeech_Timer = 15000; - else - ExecuteSpeech_Timer = 7000; - - ++ExecuteSpeech_Counter; - } - else - ExecuteSpeech_Timer -= diff; - } - } - }; -}; - -// Spell and NPC IDs for Scourge Assault event -enum NecroSpells -{ - SPELL_SCARLET_GHOUL = 52683, // Raises a Scarlet Ghoul from a humanoid corpse - SPELL_SCOURGE_GRYPHON = 52685, // Raises a Scourge Gryphon from a gryphon corpse - SPELL_GHOULPLOSION = 52672 // Causes a Gluttonous Geist to explode (kill) -}; - -enum NecroNPCs -{ - NPC_GLUTTONOUS_GEIST = 28905, - NPC_DEAD_SCARLET_MEDIC = 28895, - NPC_DEAD_SCARLET_INFANTRYMAN = 28896, - NPC_DEAD_SCARLET_CAPTAIN = 28898, - NPC_DEAD_SCARLET_PEASANT = 28892, - NPC_DEAD_SCARLET_MINER = 28891, - NPC_DEAD_SCARLET_FLEET_DEFENDER = 28886, - NPC_DEAD_SCARLET_GRYPHON = 28893 -}; - -/*###### -## npc_acherus_necromancer (Entry 28889) -######*/ -class npc_acherus_necromancer : public CreatureScript -{ -public: - npc_acherus_necromancer() : CreatureScript("npc_acherus_necromancer") { } - - struct npc_acherus_necromancerAI : public ScriptedAI - { - npc_acherus_necromancerAI(Creature* creature) : ScriptedAI(creature) { } - - EventMap events; - ObjectGuid targetCorpseGUID; - ObjectGuid geistGUID; - bool isOnRitual; - - // Event timers (IDs) - enum Events - { - EVENT_START_RITUAL = 1, - EVENT_GHOULPLOSION, - EVENT_RAISE_GHOUL, - EVENT_RESUME_WP - }; - - // Point ID for movement - enum Points - { - POINT_CORPSE_REACHED = 1 - }; - - void Reset() override - { - events.Reset(); - targetCorpseGUID.Clear(); - geistGUID.Clear(); - isOnRitual = false; - // Start waypoint movement using WaypointMovementGenerator - if (uint32 pathId = me->GetWaypointPath()) - { - me->GetMotionMaster()->MoveWaypoint(pathId, true); // true = repeatable - } - // Schedule the first ritual after 20-30s - events.ScheduleEvent(EVENT_START_RITUAL, 20s, 30s); - } - - void UpdateAI(uint32 diff) override - { - events.Update(diff); - - if (uint32 eventId = events.ExecuteEvent()) - { - switch (eventId) - { - case EVENT_START_RITUAL: - { - if (isOnRitual) // Already performing ritual - { - events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); - break; - } - - // Find nearest dead Scarlet humanoid (exclude gryphon) - Creature* nearestCorpse = nullptr; - float nearestDist = std::numeric_limits::max(); - static const uint32 corpseEntries[] = { - NPC_DEAD_SCARLET_MEDIC, NPC_DEAD_SCARLET_INFANTRYMAN, NPC_DEAD_SCARLET_CAPTAIN, - NPC_DEAD_SCARLET_PEASANT, NPC_DEAD_SCARLET_MINER, NPC_DEAD_SCARLET_FLEET_DEFENDER - }; - for (uint32 entry : corpseEntries) - { - // Search up to 60 yards for each type - if (Creature* corpse = me->FindNearestCreature(entry, 60.0f, true)) - { - float dist = me->GetDistance(corpse); - if (dist < nearestDist) - { - nearestDist = dist; - nearestCorpse = corpse; - } - } - } - if (!nearestCorpse) - { - // No corpse found nearby: try again later - events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); - break; - } - // Start ritual - isOnRitual = true; - targetCorpseGUID = nearestCorpse->GetGUID(); - geistGUID.Clear(); - // Pause waypoint movement and move to the corpse - me->PauseMovement(); - float x, y, z; - // Keep it at a distance from the corpse - nearestCorpse->GetClosePoint(x, y, z, me->GetObjectSize()); - me->GetMotionMaster()->MovePoint(POINT_CORPSE_REACHED, x, y, z); - break; - } - - case EVENT_GHOULPLOSION: - { - if (Creature* geist = ObjectAccessor::GetCreature(*me, geistGUID)) - { - me->SetFacingToObject(geist); - DoCast(geist, SPELL_GHOULPLOSION); - } - break; - } - - case EVENT_RAISE_GHOUL: - { - if (Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID)) - { - // Cast Scarlet Ghoul on the corpse (always a humanoid for necromancer) - me->SetFacingToObject(corpse); - DoCast(corpse, SPELL_SCARLET_GHOUL); - } - break; - } - - case EVENT_RESUME_WP: - { - // Resume waypoint movement - isOnRitual = false; - - targetCorpseGUID.Clear(); - - // Resume paused waypoint movement - me->ResumeMovement(); - // Schedule next ritual in 20-30s - events.ScheduleEvent(EVENT_START_RITUAL, 20s, 30s); - break; - } - } - } - - // Necromancers are not expected to engage in combat; no melee UpdateAI needed beyond events. - } - - void MovementInform(uint32 type, uint32 id) override - { - if (type == POINT_MOTION_TYPE && id == POINT_CORPSE_REACHED) - { - // Reached the corpse - // Check for nearby Gluttonous Geist within ~3 yards - Creature* geist = me->FindNearestCreature(NPC_GLUTTONOUS_GEIST, 3.0f, true); - if (geist) - { - me->SetFacingToObject(geist); - geistGUID = geist->GetGUID(); - // Geist found: schedule Ghoulplosion at +3s, then raising at +6s, then resume at +9s - events.ScheduleEvent(EVENT_GHOULPLOSION, 3s); - events.ScheduleEvent(EVENT_RAISE_GHOUL, 6s); - events.ScheduleEvent(EVENT_RESUME_WP, 9s); - } - else - { - // No Geist: just raise after 3s, resume 3s later - - Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID); - if (corpse) - { - me->SetFacingToObject(corpse); - } - - events.ScheduleEvent(EVENT_RAISE_GHOUL, 3s); - events.ScheduleEvent(EVENT_RESUME_WP, 6s); - } - } - } - }; - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_acherus_necromancerAI(creature); - } -}; - -/*###### -## npc_gothik_the_harvester (Entry 28890) -######*/ -class npc_gothik_the_harvester : public CreatureScript -{ -public: - npc_gothik_the_harvester() : CreatureScript("npc_gothik_the_harvester") { } - - struct npc_gothik_the_harvesterAI : public ScriptedAI - { - npc_gothik_the_harvesterAI(Creature* creature) : ScriptedAI(creature) { } - - EventMap events; - ObjectGuid targetCorpseGUID; - ObjectGuid geistGUID; - bool isOnRitual; - - enum Events - { - EVENT_START_RITUAL = 1, - EVENT_GHOULPLOSION, - EVENT_RAISE_DEAD, - EVENT_RESUME_WP - }; - - enum Points - { - POINT_CORPSE_REACHED = 1 - }; - - // Text identifiers for creature_text (see SQL below) - enum Says - { - SAY_GRYPHON = 0, // "You will fly again, beast..." - SAY_GHOUL = 1, // "Surprise, surprise! Another ghoul!" - SAY_GEIST = 2 // "Is Gothik the Harvester going to have to choke a geist?" - }; - - void Reset() override - { - events.Reset(); - targetCorpseGUID.Clear(); - geistGUID.Clear(); - isOnRitual = false; - // Start waypoint movement using WaypointMovementGenerator - if (uint32 pathId = me->GetWaypointPath()) - { - me->GetMotionMaster()->MoveWaypoint(pathId, true); // true = repeatable - } - // Schedule the first ritual after 50-60s - events.ScheduleEvent(EVENT_START_RITUAL, 50s, 60s); - } - void UpdateAI(uint32 diff) override - { - events.Update(diff); - - if (uint32 eventId = events.ExecuteEvent()) - { - switch (eventId) - { - case EVENT_START_RITUAL: - { - if (isOnRitual) // Already performing ritual - { - events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); - break; - } - - // Find nearest dead Scarlet NPC (including gryphon) - Creature* nearestCorpse = nullptr; - float nearestDist = std::numeric_limits::max(); - static const uint32 corpseEntries[] = { - NPC_DEAD_SCARLET_MEDIC, NPC_DEAD_SCARLET_INFANTRYMAN, NPC_DEAD_SCARLET_CAPTAIN, - NPC_DEAD_SCARLET_PEASANT, NPC_DEAD_SCARLET_MINER, NPC_DEAD_SCARLET_FLEET_DEFENDER, - NPC_DEAD_SCARLET_GRYPHON - }; - for (uint32 entry : corpseEntries) - { - // Search up to 60 yards for each type - if (Creature* corpse = me->FindNearestCreature(entry, 60.0f, true)) - { - float dist = me->GetDistance(corpse); - if (dist < nearestDist) - { - nearestDist = dist; - nearestCorpse = corpse; - } - } - } - if (!nearestCorpse) - { - events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); - break; - } - // Start ritual - isOnRitual = true; - targetCorpseGUID = nearestCorpse->GetGUID(); - geistGUID.Clear(); - // Pause waypoint movement and move to the corpse - me->PauseMovement(); - float x, y, z; - // Keep it at a distance from the corpse - nearestCorpse->GetClosePoint(x, y, z, me->GetObjectSize()); - me->GetMotionMaster()->MovePoint(POINT_CORPSE_REACHED, x, y, z); - break; - } - case EVENT_GHOULPLOSION: - { - // Cast Ghoulplosion on the Geist and say the Geist line - if (Creature* geist = ObjectAccessor::GetCreature(*me, geistGUID)) - { - Talk(SAY_GEIST); - me->SetFacingToObject(geist); - DoCast(geist, SPELL_GHOULPLOSION); - } - break; - } - - case EVENT_RAISE_DEAD: - { - // Cast the appropriate raise spell on the corpse (griffon or ghoul) - if (Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID)) - { - me->SetFacingToObject(corpse); - uint32 entry = corpse->GetEntry(); - if (entry == NPC_DEAD_SCARLET_GRYPHON) - { - DoCast(corpse, SPELL_SCOURGE_GRYPHON); - } - else - { - DoCast(corpse, SPELL_SCARLET_GHOUL); - } - } - break; - } - case EVENT_RESUME_WP: - { - // Resume waypoint movement - isOnRitual = false; - targetCorpseGUID.Clear(); - // Resume paused waypoint movement - me->ResumeMovement(); - // Schedule next ritual in 50-60s - events.ScheduleEvent(EVENT_START_RITUAL, 50s, 60s); - break; - } - } - } - } - - void MovementInform(uint32 type, uint32 id) override - { - if (type == POINT_MOTION_TYPE && id == POINT_CORPSE_REACHED) - { - // Reached the target corpse - Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID); - if (corpse) - { - me->SetFacingToObject(corpse); - // Say line depending on corpse type (gryphon or humanoid) - if (corpse->GetEntry() == NPC_DEAD_SCARLET_GRYPHON) - Talk(SAY_GRYPHON); - else - Talk(SAY_GHOUL); - } - // Check for Geist nearby - Creature* geist = me->FindNearestCreature(NPC_GLUTTONOUS_GEIST, 3.0f, true); - if (geist) - { - me->SetFacingToObject(geist); - geistGUID = geist->GetGUID(); - // Geist present: Ghoulplosion in 3s (with SAY_GEIST), raise in 6s, resume in 9s - events.ScheduleEvent(EVENT_GHOULPLOSION, 3s); - events.ScheduleEvent(EVENT_RAISE_DEAD, 6s); - events.ScheduleEvent(EVENT_RESUME_WP, 9s); - } - else - { - // No Geist: raise in 3s, resume in 6s - events.ScheduleEvent(EVENT_RAISE_DEAD, 3s); - events.ScheduleEvent(EVENT_RESUME_WP, 6s); - } - } - } - }; - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_gothik_the_harvesterAI(creature); - } -}; - -//How to win friends and influence enemies -// texts signed for creature 28939 but used for 28939, 28940, 28610 -enum win_friends -{ - SAY_CRUSADER = 1, - SAY_PERSUADED1 = 2, - SAY_PERSUADED2 = 3, - SAY_PERSUADED3 = 4, - SAY_PERSUADED4 = 5, - SAY_PERSUADED5 = 6, - SAY_PERSUADED6 = 7, - SAY_PERSUADE_RAND = 8, - QUEST_HOW_TO_WIN_FRIENDS = 12720, - - NPC_SCARLET_PREACHER = 28939, - NPC_SCARLET_COMMANDER = 28936, - NPC_SCARLET_CRUSADER = 28940, - NPC_SCARLET_MARKSMAN = 28610, - NPC_SCARLET_LORD_MCCREE = 28964 -}; - -// 52781 - Persuasive Strike -class spell_chapter2_persuasive_strike : public SpellScript -{ - PrepareSpellScript(spell_chapter2_persuasive_strike); - - bool Load() override - { - return GetCaster() && GetCaster()->IsPlayer() - && GetCaster()->ToPlayer()->GetQuestStatus(QUEST_HOW_TO_WIN_FRIENDS) == QUEST_STATUS_INCOMPLETE; - } - - void HandleHit(SpellEffIndex /*effIndex*/) - { - Creature* creature = GetHitCreature(); - Player* player = GetCaster()->ToPlayer(); - - if (!creature || !player) - return; - - if (!creature->EntryEquals(NPC_SCARLET_PREACHER, NPC_SCARLET_COMMANDER, NPC_SCARLET_CRUSADER, NPC_SCARLET_MARKSMAN, NPC_SCARLET_LORD_MCCREE)) - return; - - sCreatureTextMgr->SendChat(creature, SAY_PERSUADE_RAND, nullptr, CHAT_MSG_ADDON, LANG_ADDON, TEXT_RANGE_NORMAL, 0, TEAM_NEUTRAL, false, player); - - if (roll_chance_f(30.0f)) - { - creature->CombatStop(true); - creature->GetMotionMaster()->MoveIdle(); - creature->SetImmuneToPC(true); - creature->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); - creature->SetReactState(REACT_PASSIVE); - - creature->AI()->Talk(SAY_PERSUADED1, 8s); - creature->AI()->Talk(SAY_PERSUADED2, 16s); - creature->AI()->Talk(SAY_PERSUADED3, 24s); - creature->AI()->Talk(SAY_PERSUADED4, 32s); - - ObjectGuid playerGuid = player->GetGUID(); - - creature->m_Events.AddEventAtOffset([creature, playerGuid] - { - if (Player* caster = ObjectAccessor::GetPlayer(*creature, playerGuid)) - sCreatureTextMgr->SendChat(creature, SAY_PERSUADED5, nullptr, CHAT_MSG_ADDON, LANG_ADDON, TEXT_RANGE_NORMAL, 0, TEAM_NEUTRAL, false, caster); - }, 40s); - - creature->m_Events.AddEventAtOffset([creature, playerGuid] - { - creature->AI()->Talk(SAY_PERSUADED6); - - if (Player* caster = ObjectAccessor::GetPlayer(*creature, playerGuid)) - { - Unit::Kill(caster, creature); - caster->GroupEventHappens(QUEST_HOW_TO_WIN_FRIENDS, creature); - } - else - creature->KillSelf(); - }, 48s); - } - else - creature->AI()->Talk(SAY_CRUSADER, 1s); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_chapter2_persuasive_strike::HandleHit, EFFECT_0, SPELL_EFFECT_DUMMY); - } -}; - -enum AcherusPortal -{ - SPELL_PORTAL_EFFECT_ACHERUS = 53098, - QUEST_SCARLET_ARMIES_APPROACH = 12757 -}; - -class spell_portal_effect_acherus : public SpellScript -{ - PrepareSpellScript(spell_portal_effect_acherus); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_PORTAL_EFFECT_ACHERUS }); - } - - SpellCastResult CheckCast() - { - Unit* target = GetExplTargetUnit(); - if (target && target->IsPlayer() && target->ToPlayer()->HasQuest(QUEST_SCARLET_ARMIES_APPROACH)) - return SPELL_CAST_OK; - - return SPELL_FAILED_DONT_REPORT; - } - - void HandleScriptEffect(SpellEffIndex /*effIndex*/) - { - if (Unit* caster = GetCaster()) - if (Player* player = GetHitPlayer()) - caster->CastSpell(player, GetEffectValue(), true); - } - - void Register() override - { - OnCheckCast += SpellCheckCastFn(spell_portal_effect_acherus::CheckCast); - OnEffectHitTarget += SpellEffectFn(spell_portal_effect_acherus::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -void AddSC_the_scarlet_enclave_c2() -{ - new npc_scarlet_courier(); - new npc_koltira_deathweaver(); - new npc_a_special_surprise(); - new npc_acherus_necromancer(); - new npc_gothik_the_harvester(); - RegisterSpellScript(spell_chapter2_persuasive_strike); - RegisterSpellScript(spell_portal_effect_acherus); -} diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter3.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter3.cpp deleted file mode 100644 index bae74529e..000000000 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter3.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#include "CreatureScript.h" -#include "ScriptedCreature.h" -#include "SpellAuras.h" -#include "SpellInfo.h" -#include "SpellScript.h" -#include "SpellScriptLoader.h" - -enum DevourHumanoid -{ - NPC_HEARTHGLEN_CRUSADER = 29102, - NPC_TIRISFAL_CRUSADER = 29103 -}; - -// 53110 - Devour Humanoid -class spell_q12779_an_end_to_all_things : public SpellScript -{ - PrepareSpellScript(spell_q12779_an_end_to_all_things); - - SpellCastResult CheckCast() - { - if (Unit* caster = GetCaster()) - if (caster->FindNearestCreature(NPC_HEARTHGLEN_CRUSADER, 15.0f, true) || caster->FindNearestCreature(NPC_TIRISFAL_CRUSADER, 15.0f, true)) - return SPELL_CAST_OK; - - return SPELL_FAILED_BAD_TARGETS; - } - - void HandleScriptEffect(SpellEffIndex /*effIndex*/) - { - if (Creature* c = GetHitUnit()->ToCreature()) - if (Unit* caster = GetCaster()) - { - c->AI()->AttackStart(caster); - c->CastSpell(caster, GetEffectValue(), true); // 53111 - } - } - - void Register() override - { - OnCheckCast += SpellCheckCastFn(spell_q12779_an_end_to_all_things::CheckCast); - OnEffectHitTarget += SpellEffectFn(spell_q12779_an_end_to_all_things::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); - } -}; - -// 53111 - Devour Humanoid (casted by the devoured creature) -class spell_q12779_an_end_to_all_things_devour_aura : public AuraScript -{ - PrepareAuraScript(spell_q12779_an_end_to_all_things_devour_aura); - - void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) - { - Unit* caster = GetCaster(); - Unit* target = GetTarget(); - if (!caster || !target) - return; - - if (GetTargetApplication()->GetRemoveMode() == AURA_REMOVE_BY_EXPIRE) - { - caster->SetDisableGravity(true); - Unit::Kill(target, caster); - } - } - - void Register() override - { - AfterEffectRemove += AuraEffectRemoveFn(spell_q12779_an_end_to_all_things_devour_aura::OnRemove, EFFECT_0, SPELL_AURA_CONTROL_VEHICLE, AURA_EFFECT_HANDLE_REAL); - } -}; - -void AddSC_the_scarlet_enclave_c3() -{ - RegisterSpellScript(spell_q12779_an_end_to_all_things); - RegisterSpellScript(spell_q12779_an_end_to_all_things_devour_aura); -} diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter5.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter5.cpp deleted file mode 100644 index 7b6d04c18..000000000 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter5.cpp +++ /dev/null @@ -1,1317 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#include "CreatureScript.h" -#include "Player.h" -#include "ScriptedCreature.h" -#include "ScriptedGossip.h" -#include "SpellScript.h" -#include "SpellScriptLoader.h" -#include "WorldStateDefines.h" - -enum LightOfDawnSays -{ - SAY_LIGHT_OF_DAWN01 = 0, // pre text - SAY_LIGHT_OF_DAWN02 = 1, - SAY_LIGHT_OF_DAWN03 = 2, - SAY_LIGHT_OF_DAWN04 = 3, // intro - SAY_LIGHT_OF_DAWN05 = 4, - SAY_LIGHT_OF_DAWN06 = 5, - SAY_LIGHT_OF_DAWN07 = 6, // During the fight - Korfax, Champion of the Light - SAY_LIGHT_OF_DAWN08 = 7, // Lord Maxwell Tyrosus - SAY_LIGHT_OF_DAWN09 = 8, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN25 = 24, // After the fight - SAY_LIGHT_OF_DAWN26 = 25, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN27 = 26, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN28 = 27, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN29 = 28, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN30 = 29, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN31 = 30, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN32 = 31, // Highlord Alexandros Mograine - SAY_LIGHT_OF_DAWN33 = 32, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN34 = 33, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN35 = 34, // Darion Mograine - SAY_LIGHT_OF_DAWN36 = 35, // Darion Mograine - SAY_LIGHT_OF_DAWN37 = 36, // Highlord Alexandros Mograine - SAY_LIGHT_OF_DAWN38 = 37, // Darion Mograine - SAY_LIGHT_OF_DAWN39 = 38, // Highlord Alexandros Mograine - SAY_LIGHT_OF_DAWN40 = 39, // Darion Mograine - SAY_LIGHT_OF_DAWN41 = 40, // Highlord Alexandros Mograine - SAY_LIGHT_OF_DAWN42 = 41, // Highlord Alexandros Mograine - SAY_LIGHT_OF_DAWN43 = 42, // The Lich King - SAY_LIGHT_OF_DAWN44 = 43, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN45 = 44, // The Lich King - SAY_LIGHT_OF_DAWN46 = 45, // The Lich King - SAY_LIGHT_OF_DAWN47 = 46, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN48 = 47, // The Lich King - SAY_LIGHT_OF_DAWN49 = 48, // The Lich King - SAY_LIGHT_OF_DAWN50 = 49, // Lord Maxwell Tyrosus - SAY_LIGHT_OF_DAWN51 = 50, // The Lich King - SAY_LIGHT_OF_DAWN52 = 51, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN53 = 52, // Highlord Darion Mograine - SAY_LIGHT_OF_DAWN54 = 53, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN55 = 54, // The Lich King - SAY_LIGHT_OF_DAWN56 = 55, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN57 = 56, // The Lich King - SAY_LIGHT_OF_DAWN58 = 57, // The Lich King - SAY_LIGHT_OF_DAWN59 = 58, // The Lich King - SAY_LIGHT_OF_DAWN60 = 59, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN61 = 60, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN62 = 61, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN63 = 62, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN64 = 63, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN65 = 64, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN66 = 65, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN67 = 66, // Highlord Tirion Fordring - SAY_LIGHT_OF_DAWN68 = 67, // Highlord Darion Mograine - - EMOTE_LIGHT_OF_DAWN01 = 68, // Emotes - EMOTE_LIGHT_OF_DAWN02 = 69, - EMOTE_LIGHT_OF_DAWN03 = 70, - EMOTE_LIGHT_OF_DAWN04 = 71, - EMOTE_LIGHT_OF_DAWN05 = 72, - EMOTE_LIGHT_OF_DAWN06 = 73, - EMOTE_LIGHT_OF_DAWN07 = 74, - EMOTE_LIGHT_OF_DAWN08 = 75, - EMOTE_LIGHT_OF_DAWN09 = 76, - EMOTE_LIGHT_OF_DAWN10 = 77, - EMOTE_LIGHT_OF_DAWN11 = 78, - EMOTE_LIGHT_OF_DAWN12 = 79, - EMOTE_LIGHT_OF_DAWN13 = 80, - EMOTE_LIGHT_OF_DAWN14 = 81, - EMOTE_LIGHT_OF_DAWN15 = 82, - EMOTE_LIGHT_OF_DAWN16 = 83, - EMOTE_LIGHT_OF_DAWN17 = 84, - EMOTE_LIGHT_OF_DAWN18 = 85 -}; - -enum LightOfDawnEncounter -{ - // Intro Events - EVENT_START_COUNTDOWN_1 = 1, - EVENT_START_COUNTDOWN_2, - EVENT_START_COUNTDOWN_3, - EVENT_START_COUNTDOWN_4, - EVENT_START_COUNTDOWN_5, - EVENT_START_COUNTDOWN_6, - EVENT_START_COUNTDOWN_7, - EVENT_START_COUNTDOWN_8, - EVENT_START_COUNTDOWN_9, - EVENT_START_COUNTDOWN_10, - EVENT_START_COUNTDOWN_11, - EVENT_START_COUNTDOWN_12, - EVENT_START_COUNTDOWN_13, - EVENT_START_COUNTDOWN_14, - // Fight Events - EVENT_SPELL_ANTI_MAGIC_ZONE, - EVENT_SPELL_DEATH_STRIKE, - EVENT_SPELL_DEATH_EMBRACE, - EVENT_SPELL_UNHOLY_BLIGHT, - EVENT_SPELL_DARION_MOD_DAMAGE, - // Positioning - EVENT_FINISH_FIGHT_1, - EVENT_FINISH_FIGHT_2, - EVENT_FINISH_FIGHT_3, - EVENT_FINISH_FIGHT_4, - EVENT_FINISH_FIGHT_5, - // Outro - EVENT_OUTRO_SCENE_1, - EVENT_OUTRO_SCENE_2, - EVENT_OUTRO_SCENE_3, - EVENT_OUTRO_SCENE_4, - EVENT_OUTRO_SCENE_5, - EVENT_OUTRO_SCENE_6, - EVENT_OUTRO_SCENE_7, - EVENT_OUTRO_SCENE_8, - EVENT_OUTRO_SCENE_9, - EVENT_OUTRO_SCENE_10, - EVENT_OUTRO_SCENE_11, - EVENT_OUTRO_SCENE_12, - EVENT_OUTRO_SCENE_13, - EVENT_OUTRO_SCENE_14, - EVENT_OUTRO_SCENE_15, - EVENT_OUTRO_SCENE_16, - EVENT_OUTRO_SCENE_17, - EVENT_OUTRO_SCENE_18, - EVENT_OUTRO_SCENE_19, - EVENT_OUTRO_SCENE_20, - EVENT_OUTRO_SCENE_21, - EVENT_OUTRO_SCENE_22, - EVENT_OUTRO_SCENE_23, - EVENT_OUTRO_SCENE_24, - EVENT_OUTRO_SCENE_25, - EVENT_OUTRO_SCENE_26, - EVENT_OUTRO_SCENE_27, - EVENT_OUTRO_SCENE_28, - EVENT_OUTRO_SCENE_29, - EVENT_OUTRO_SCENE_30, - EVENT_OUTRO_SCENE_31, - EVENT_OUTRO_SCENE_32, - EVENT_OUTRO_SCENE_33, - EVENT_OUTRO_SCENE_34, - EVENT_OUTRO_SCENE_35, - EVENT_OUTRO_SCENE_36, - EVENT_OUTRO_SCENE_37, - EVENT_OUTRO_SCENE_38, - EVENT_OUTRO_SCENE_39, - EVENT_OUTRO_SCENE_40, - EVENT_OUTRO_SCENE_41, - EVENT_OUTRO_SCENE_42, - EVENT_OUTRO_SCENE_43, - EVENT_OUTRO_SCENE_44, - EVENT_OUTRO_SCENE_45, - EVENT_OUTRO_SCENE_46, - EVENT_OUTRO_SCENE_47, - EVENT_OUTRO_SCENE_48, - EVENT_OUTRO_SCENE_49, - EVENT_OUTRO_SCENE_50, - EVENT_OUTRO_SCENE_51, - EVENT_OUTRO_SCENE_52, - EVENT_OUTRO_SCENE_53, - EVENT_OUTRO_SCENE_54, - EVENT_OUTRO_SCENE_55, - EVENT_OUTRO_SCENE_56, - EVENT_OUTRO_SCENE_57, - EVENT_OUTRO_SCENE_58, - EVENT_OUTRO_SCENE_59, - EVENT_OUTRO_SCENE_60, - EVENT_OUTRO_SCENE_61, - - ACTION_START_EVENT = 1, - ACTION_PLAY_EMOTE = 1, - ACTION_POSITION_NPCS = 2, - - ENCOUNTER_START_TIME = 5, - ENCOUNTER_TOTAL_DEFENDERS = 300, - ENCOUNTER_TOTAL_SCOURGE = 10000, - - ENCOUNTER_STATE_NONE = 0, - ENCOUNTER_STATE_FIGHT = 1, - ENCOUNTER_STATE_OUTRO = 2, -}; - -enum LightOfDawnNPCs -{ - // Defenders - NPC_DEFENDER_OF_THE_LIGHT = 29174, - NPC_KORFAX_CHAMPION_OF_THE_LIGHT = 29176, - NPC_COMMANDER_ELIGOR_DAWNBRINGER = 29177, - NPC_LORD_MAXWELL_TYROSUS = 29178, - NPC_LEONID_BARTHALOMEW_THE_REVERED = 29179, - NPC_DUKE_NICHOLAS_ZVERENHOFF = 29180, - NPC_RAYNE = 29181, - NPC_RIMBLAT_EARTHSHATTER = 29182, - - // Scourge - NPC_RAMPAGING_ABOMINATION = 29186, - NPC_ACHERUS_GHOUL = 29219, - NPC_WARRIOR_OF_THE_FROZEN_WASTES = 29206, - NPC_FLESH_BEHEMOTH = 29190, - - NPC_HIGHLORD_DARION_MOGRAINE = 29173, - NPC_KOLTIRA_DEATHWEAVER = 29199, - NPC_ORBAZ_BLOODBANE = 29204, - NPC_THASSARIAN = 29200, - - // Outro - NPC_HIGHLORD_TIRION_FORDRING = 29175, - NPC_HIGHLORD_ALEXANDROS_MOGRAINE = 29227, // ghost - NPC_DARION_MOGRAINE = 29228, // ghost - NPC_THE_LICH_KING = 29183, -}; - -enum LightOfDawnGOs -{ - GO_HOLY_LIGHTNING = 191301, - GO_LIGHT_OF_DAWN = 191330 -}; - -enum LightOfDawnSpells -{ - // Intro Spells - SPELL_CAMERA_SHAKE_INIT = 36455, - SPELL_CAMERA_SHAKE = 39983, - SPELL_THE_MIGHT_OF_MOGRAINE = 53642, - - // Mograine Fight - SPELL_ANTI_MAGIC_ZONE1 = 52893, - SPELL_DEATH_STRIKE = 53639, - SPELL_DEATH_EMBRACE = 53635, - SPELL_ICY_TOUCH1 = 49723, - SPELL_UNHOLY_BLIGHT = 53640, - SPELL_DARION_MOD_DAMAGE = 53645, - - // Outro - SPELL_THE_LIGHT_OF_DAWN = 53658, - SPELL_ALEXANDROS_MOGRAINE_SPAWN = 53667, - SPELL_ICEBOUND_VISAGE = 53274, - SPELL_SOUL_FEAST_ALEX = 53677, - SPELL_MOGRAINE_CHARGE = 53679, - SPELL_REBUKE = 53680, - SPELL_SOUL_FEAST_TIRION = 53685, - SPELL_APOCALYPSE = 53210, - SPELL_THROW_ASHBRINGER = 53701, - SPELL_REBIRTH_OF_THE_ASHBRINGER = 53702, - SPELL_TIRION_CHARGE = 53705, - SPELL_EXIT_TELEPORT_VISUAL = 61456, - SPELL_LAY_ON_HANDS = 53778, - SPELL_THE_LIGHT_OF_DAWN_Q = 53606 -}; - -const Position LightOfDawnPos[] = -{ - {2304.2f, -5290.7f, 82.01f, 4.56f}, // 0 First Home Pos - {2253.5f, -5310.6f, 82.17f, 5.28f}, // 1 Second Home Pos - {2169.1f, -5227.1f, 82.59f, 5.7f}, // 2 Orbaz Flee Pos - {2289.259f, -5280.355f, 86.112f, 4.41f}, // 3 Koltira Loc1 - {2273.289f, -5273.675f, 86.701f, 5.01f}, // 4 Thassarian Loc1 - {2280.81f, -5284.09f, 86.608f, 4.76f}, // 5 Morgraine Loc1 - {2165.711f, -5266.1235f, 95.5025f, 0.13962634f }, // 6 Tirion Summon loc - {2281.198f, -5257.397f, 80.224f, 4.66f}, // 7 Alexandros loc1 - {2281.156f, -5259.934f, 80.647f, 0}, // 8 Alexandros loc2 - {2281.294f, -5281.895f, 82.445f, 1.35f}, // 9 Darion loc1 - {2281.093f, -5263.013f, 81.125f, 0}, // 10 Darion loc2 - {2283.896f, -5287.914f, 83.066f, 1.55f}, // 11 Tirion Fordring loc2 - {2280.304f, -5257.205f, 80.09781f, 4.6251f },// 12 Lich King spawns - {2280.687f, -5262.276f, 81.082634f, 0.0f },// 13 Lich king moves forward - {2264.27f, -5267.29f, 80.16f, 0}, // 14 Tirion Fordring loc3 - {2270.99f, -5278.00f, 81.89f, 0} // 15 Tirion Fordring loc4 -}; - -const Position LightOfDawnFightPos[] = -{ - {2279.68f, -5256.75f, 79.79f, 4.8f}, - {2280.40f, -5276.56f, 82.11f, 4.8f}, - {2256.43f, -5281.3f, 82.29f, 5.0f}, - {2251.87f, -5304.08f, 82.17f, 4.8f}, - {2244.88f, -5256.03f, 74.88f, 5.8f}, - {2294.29f, -5281.35f, 81.91f, 4.8f}, - {2314.2f, -5268.1f, 82.43f, 3.6f}, - {2289.72f, -5299.65f, 83.49f, 3.2f}, - {2274.02f, -5303.58f, 85.05f, 1.4f}, - {2258.42f, -5307.72f, 81.98f, 0.1f} -}; - -class DelayedSummonEvent : public BasicEvent -{ -public: - DelayedSummonEvent(Unit* owner, uint32 entry, Position pos) : _owner(owner), _entry(entry), _pos(pos) { } - - bool Execute(uint64 /*eventTime*/, uint32 /*updateTime*/) override - { - _owner->SummonCreature(_entry, _pos, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 3000); - return true; - } - -private: - Unit* _owner; - uint32 _entry; - Position _pos; -}; - -class npc_highlord_darion_mograine : public CreatureScript -{ -public: - npc_highlord_darion_mograine() : CreatureScript("npc_highlord_darion_mograine") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_highlord_darion_mograineAI(creature); - } - - bool OnGossipHello(Player* player, Creature* creature) override - { - if (creature->IsQuestGiver()) - player->PrepareQuestMenu(creature->GetGUID()); - - if (player->GetQuestStatus(12801) == QUEST_STATUS_INCOMPLETE && !creature->AI()->GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE)) - AddGossipItemFor(player, 9795, 0, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); - - SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID()); - - return true; - } - - bool OnGossipSelect(Player* player, Creature* creature, uint32 /*sender*/, uint32 action) override - { - if (action == GOSSIP_ACTION_INFO_DEF + 1) - { - ClearGossipMenuFor(player); - CloseGossipMenuFor(player); - creature->AI()->DoAction(ACTION_START_EVENT); - } - return true; - } - - struct npc_highlord_darion_mograineAI : public ScriptedAI - { - npc_highlord_darion_mograineAI(Creature* creature) : ScriptedAI(creature), summons(me) - { - battleStarted = ENCOUNTER_STATE_NONE; - me->SetCorpseDelay(3 * 60); - me->SetRespawnTime(3 * 60); - resetExecuted = false; - } - - EventMap events; - SummonList summons; - uint32 startTimeRemaining; - uint32 defendersRemaining; - uint32 scourgeRemaining; - uint8 battleStarted; - bool resetExecuted; - - void DoAction(int32 param) override - { - if (param == ACTION_START_EVENT && !startTimeRemaining && events.Empty()) - { - Talk(SAY_LIGHT_OF_DAWN01); - - startTimeRemaining = ENCOUNTER_START_TIME; - defendersRemaining = ENCOUNTER_TOTAL_DEFENDERS; - scourgeRemaining = ENCOUNTER_TOTAL_SCOURGE; - - SendInitialWorldStates(); - - events.Reset(); - events.ScheduleEvent(EVENT_START_COUNTDOWN_1, 60s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_2, 120s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_3, 180s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_4, 240s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_5, 300s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_6, 308s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_7, 312s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_8, 316s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_9, 320s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_10, 324s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_11, 332s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_12, 335s); - events.ScheduleEvent(EVENT_START_COUNTDOWN_13, 337s + 500ms); - events.ScheduleEvent(EVENT_START_COUNTDOWN_14, 345s); - } - } - - uint32 GetData(uint32 type) const override - { - switch (type) - { - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT: - return defendersRemaining; - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT: - return scourgeRemaining; - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE: - return me->IsAlive() && (startTimeRemaining || battleStarted); - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE: - return me->IsAlive() && startTimeRemaining; - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME: - return startTimeRemaining; - case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE: - return me->IsAlive() && !startTimeRemaining && battleStarted; - } - return 0; - } - - void SendUpdateWorldState(uint32 id, uint32 state) - { - Map::PlayerList const& players = me->GetMap()->GetPlayers(); - if (!players.IsEmpty()) - for (Map::PlayerList::const_iterator itr = players.begin(); itr != players.end(); ++itr) - if (Player* player = itr->GetSource()) - if (player->GetPhaseMask() & 128) // Xinef: client skips players without chapter 5 aura anyway, speedup - player->SendUpdateWorldState(id, state); - } - - void SendInitialWorldStates() - { - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT)); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT)); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE)); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE)); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME)); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE)); - } - - void JustSummoned(Creature* cr) override - { - summons.Summon(cr); - - if (me->IsInCombat() && cr->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING && battleStarted == ENCOUNTER_STATE_FIGHT) - { - Position pos = LightOfDawnFightPos[urand(0, 9)]; - if (Unit* target = cr->SelectNearbyTarget(nullptr, 10.0f)) - if (target->IsCreature()) - target->GetMotionMaster()->MoveCharge(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), me->GetSpeed(MOVE_RUN)); - cr->GetMotionMaster()->MoveCharge(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), me->GetSpeed(MOVE_RUN)); - } - - if (battleStarted == ENCOUNTER_STATE_OUTRO && cr->GetEntry() == NPC_DEFENDER_OF_THE_LIGHT) - { - cr->SetReactState(REACT_PASSIVE); - cr->SetImmuneToAll(true); - cr->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_READY1H); - cr->HandleEmoteCommand(EMOTE_STATE_READY1H); - } - } - - void SummonedCreatureDies(Creature* creature, Unit*) override - { - // Refill Armies and update counters - if (battleStarted != ENCOUNTER_STATE_FIGHT) - return; - - me->m_Events.AddEventAtOffset(new DelayedSummonEvent(me, creature->GetEntry(), *creature), 3s); - if (creature->GetEntry() >= NPC_RAMPAGING_ABOMINATION) - { - --scourgeRemaining; - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT)); - } - else - { - --defendersRemaining; - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT)); - - if (defendersRemaining == 200) - FinishFight(); - } - } - - void JustDied(Unit*) override - { - summons.DespawnAll(); - me->SetCorpseDelay(3 * 60); - me->SetRespawnTime(3 * 60); - } - - void FinishFight() - { - if (Creature* tirion = me->SummonCreature(NPC_HIGHLORD_TIRION_FORDRING, LightOfDawnPos[6], TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 600000)) - { - tirion->LoadEquipment(0, true); - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN25, 4s); - - tirion->m_Events.AddEventAtOffset([&, tirion] { - tirion->GetMotionMaster()->MoveWaypoint(NPC_HIGHLORD_TIRION_FORDRING * 10, false); - }, 14s); - - events.Reset(); - events.ScheduleEvent(EVENT_FINISH_FIGHT_1, 10s); - events.ScheduleEvent(EVENT_FINISH_FIGHT_2, 20s); - events.ScheduleEvent(EVENT_FINISH_FIGHT_3, 22s); - events.ScheduleEvent(EVENT_FINISH_FIGHT_4, 23s); - events.ScheduleEvent(EVENT_FINISH_FIGHT_5, 24s); - - tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2254.84f, -5298.75f, 82.168f, 1.134f, 0, 0, 0.537102f, 0.843517f, 20); - tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2296.24f, -5296.44f, 81.9964f, 5.3398f, 0, 0, 0.454395f, -0.8908f, 20); - tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2314.29f, -5261.78f, 83.1349f, 3.05822f, 0, 0, 0.999131f, 0.0416735f, 20); - tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2278.43f, -5270.14f, 81.7247f, 0.70988f, 0, 0, 0.347534f, 0.937667f, 20); - } - } - - void JustEngagedWith(Unit*) override - { - if (battleStarted != ENCOUNTER_STATE_FIGHT) - return; - - events.RescheduleEvent(EVENT_SPELL_ANTI_MAGIC_ZONE, 15s); - events.RescheduleEvent(EVENT_SPELL_DEATH_STRIKE, 8s); - events.RescheduleEvent(EVENT_SPELL_DEATH_EMBRACE, 5s); - events.RescheduleEvent(EVENT_SPELL_UNHOLY_BLIGHT, 10s); - events.RescheduleEvent(EVENT_SPELL_DARION_MOD_DAMAGE, 500ms); - } - - void Reset() override - { - if (resetExecuted) - return; - - resetExecuted = true; - JustRespawned(); - } - - void JustRespawned() override - { - events.Reset(); - summons.DespawnAll(); - - me->SetImmuneToAll(true); - me->LoadEquipment(1, true); - me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_GOSSIP | UNIT_NPC_FLAG_QUESTGIVER); - me->SetStandState(UNIT_STAND_STATE_STAND); - me->SetVisible(true); - me->setActive(true); - me->SetWalk(false); - - battleStarted = ENCOUNTER_STATE_NONE; - startTimeRemaining = 0; - defendersRemaining = 0; - scourgeRemaining = 0; - - SendInitialWorldStates(); - me->SummonCreatureGroup(30); - } - - Creature* GetEntryFromSummons(uint32 entry) - { - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - if (summon->GetEntry() == entry) - return summon; - return nullptr; - } - - void MovementInform(uint32 type, uint32 point) override - { - if (type == POINT_MOTION_TYPE && point == 2) - { - me->RemoveAurasDueToSpell(SPELL_THE_LIGHT_OF_DAWN); - Talk(EMOTE_LIGHT_OF_DAWN05); - events.Reset(); - - events.ScheduleEvent(EVENT_OUTRO_SCENE_1, 2s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_2, 19s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_3, 38s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_4, 50s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_5, 62s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_6, 68s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_7, 71s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_8, 72s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_9, 74s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_10, 77s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_11, 79s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_12, 82s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_13, 85s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_14, 92s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_15, 98s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_16, 105s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_17, 120s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_18, 131s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_19, 158s); - } - } - - void UpdateAI(uint32 diff) override - { - events.Update(diff); - uint32 eventId = events.ExecuteEvent(); - - switch (eventId) - { - case EVENT_START_COUNTDOWN_1: - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 4); - break; - case EVENT_START_COUNTDOWN_2: - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 3); - break; - case EVENT_START_COUNTDOWN_3: - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 2); - break; - case EVENT_START_COUNTDOWN_4: - Talk(SAY_LIGHT_OF_DAWN02); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 1); - break; - case EVENT_START_COUNTDOWN_5: - battleStarted = ENCOUNTER_STATE_FIGHT; - me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_NONE); - Talk(SAY_LIGHT_OF_DAWN04); // Wrong order in DB! - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 0); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE, 0); - SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE, 1); - break; - case EVENT_START_COUNTDOWN_6: - case EVENT_START_COUNTDOWN_7: - case EVENT_START_COUNTDOWN_8: - case EVENT_START_COUNTDOWN_9: - case EVENT_START_COUNTDOWN_10: - if (eventId == EVENT_START_COUNTDOWN_6) - { - Talk(SAY_LIGHT_OF_DAWN05); - me->CastSpell(me, SPELL_CAMERA_SHAKE_INIT, true); - } - else - me->CastSpell(me, SPELL_CAMERA_SHAKE, true); - me->SummonCreatureGroup(eventId - EVENT_START_COUNTDOWN_6); - break; - case EVENT_START_COUNTDOWN_11: - Talk(SAY_LIGHT_OF_DAWN06); - break; - case EVENT_START_COUNTDOWN_12: - summons.DoAction(ACTION_PLAY_EMOTE); - break; - case EVENT_START_COUNTDOWN_13: - { - uint8 first = 1; - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - { - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - { - Position pos = LightOfDawnPos[first]; - summon->SetHomePosition(pos); - summon->GetMotionMaster()->MovePoint(1, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, true, false); - } - first = first == 0 ? 1 : 0; - } - Position pos = LightOfDawnPos[first]; - me->SetHomePosition(pos); - me->SetWalk(false); - me->GetMotionMaster()->MovePoint(1, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, true, true); - DoCastSelf(SPELL_THE_MIGHT_OF_MOGRAINE, true); - break; - } - case EVENT_START_COUNTDOWN_14: - me->SetImmuneToAll(false); - me->SummonCreatureGroup(5); - return; - case EVENT_FINISH_FIGHT_1: - summons.DespawnEntry(NPC_DEFENDER_OF_THE_LIGHT); - battleStarted = ENCOUNTER_STATE_OUTRO; - break; - case EVENT_FINISH_FIGHT_2: - { - summons.DespawnEntry(NPC_RAMPAGING_ABOMINATION); - summons.DespawnEntry(NPC_ACHERUS_GHOUL); - summons.DespawnEntry(NPC_WARRIOR_OF_THE_FROZEN_WASTES); - summons.DespawnEntry(NPC_FLESH_BEHEMOTH); - summons.DespawnEntry(NPC_DEFENDER_OF_THE_LIGHT); - - if (Creature* orbaz = GetEntryFromSummons(NPC_ORBAZ_BLOODBANE)) - { - orbaz->SetReactState(REACT_PASSIVE); - orbaz->AI()->Talk(EMOTE_LIGHT_OF_DAWN04); - orbaz->GetMotionMaster()->MovePoint(2, LightOfDawnPos[2], FORCED_MOVEMENT_NONE, 0.f, true, true); - orbaz->DespawnOrUnsummon(7s); - } - - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - { - summon->CombatStop(true); - summon->GetThreatMgr().ClearAllThreat(); - summon->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); - summon->SetImmuneToAll(true); - summon->SetReactState(REACT_PASSIVE); - summon->GetMotionMaster()->Clear(false); - } - me->CombatStop(true); - me->GetThreatMgr().ClearAllThreat(); - me->SetImmuneToAll(true); - me->SetReactState(REACT_PASSIVE); - me->GetMotionMaster()->Clear(false); - - // Position main stars - summons.DoAction(ACTION_POSITION_NPCS); - - me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2276.66f, -5273.60f, 81.86f, 5.14f, TEMPSUMMON_CORPSE_DESPAWN); - me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2272.11f, -5279.08f, 82.01f, 5.69f, TEMPSUMMON_CORPSE_DESPAWN); - me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2285.11f, -5276.73f, 82.08f, 4.23f, TEMPSUMMON_CORPSE_DESPAWN); - me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2290.06f, -5286.41f, 82.51f, 3.16f, TEMPSUMMON_CORPSE_DESPAWN); - break; - } - case EVENT_FINISH_FIGHT_3: - if (Creature* koltira = GetEntryFromSummons(NPC_KOLTIRA_DEATHWEAVER)) - { - koltira->SetWalk(true); - koltira->SetHomePosition(*koltira); - koltira->CastSpell(koltira, SPELL_THE_LIGHT_OF_DAWN, false); - koltira->GetMotionMaster()->MoveCharge(LightOfDawnPos[3].GetPositionX(), LightOfDawnPos[3].GetPositionY(), LightOfDawnPos[3].GetPositionZ(), 4.0f, 2); - } - break; - case EVENT_FINISH_FIGHT_4: - if (Creature* thassarin = GetEntryFromSummons(NPC_THASSARIAN)) - { - thassarin->SetWalk(true); - thassarin->SetHomePosition(*thassarin); - thassarin->CastSpell(thassarin, SPELL_THE_LIGHT_OF_DAWN, false); - thassarin->GetMotionMaster()->MoveCharge(LightOfDawnPos[4].GetPositionX(), LightOfDawnPos[4].GetPositionY(), LightOfDawnPos[4].GetPositionZ(), 4.0f, 2); - } - break; - case EVENT_FINISH_FIGHT_5: - me->SetWalk(true); - me->SetHomePosition(*me); - me->RemoveAllAuras(); - me->CastSpell(me, SPELL_THE_LIGHT_OF_DAWN, false); - me->GetMotionMaster()->MoveCharge(LightOfDawnPos[5].GetPositionX(), LightOfDawnPos[5].GetPositionY(), LightOfDawnPos[5].GetPositionZ(), 4.0f, 2); - - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN26); - break; - case EVENT_OUTRO_SCENE_1: - me->SetStandState(UNIT_STAND_STATE_KNEEL); - me->SetFacingTo(4.8f); - Talk(SAY_LIGHT_OF_DAWN27); - break; - case EVENT_OUTRO_SCENE_2: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN28); - break; - case EVENT_OUTRO_SCENE_3: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN29); - break; - case EVENT_OUTRO_SCENE_4: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN30); - break; - case EVENT_OUTRO_SCENE_5: - me->SetStandState(UNIT_STAND_STATE_STAND); - Talk(SAY_LIGHT_OF_DAWN31); - break; - case EVENT_OUTRO_SCENE_6: - if (Creature* alex = me->SummonCreature(NPC_HIGHLORD_ALEXANDROS_MOGRAINE, LightOfDawnPos[7].GetPositionX(), LightOfDawnPos[7].GetPositionY(), LightOfDawnPos[7].GetPositionZ(), LightOfDawnPos[7].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) - { - alex->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); - alex->GetMotionMaster()->MovePoint(0, LightOfDawnPos[8].GetPositionX(), LightOfDawnPos[8].GetPositionY(), LightOfDawnPos[8].GetPositionZ()); - alex->CastSpell(alex, SPELL_ALEXANDROS_MOGRAINE_SPAWN, true); - //alex->AI()->Talk(EMOTE_LIGHT_OF_DAWN06); - } - break; - case EVENT_OUTRO_SCENE_7: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - { - alex->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); - alex->AI()->Talk(SAY_LIGHT_OF_DAWN32); - me->SetFacingToObject(alex); - } - break; - case EVENT_OUTRO_SCENE_8: - Talk(SAY_LIGHT_OF_DAWN33); - break; - case EVENT_OUTRO_SCENE_9: - me->SetStandState(UNIT_STAND_STATE_KNEEL); - Talk(SAY_LIGHT_OF_DAWN34); - break; - case EVENT_OUTRO_SCENE_10: - if (Creature* darion = me->SummonCreature(NPC_DARION_MOGRAINE, LightOfDawnPos[9].GetPositionX(), LightOfDawnPos[9].GetPositionY(), LightOfDawnPos[9].GetPositionZ(), LightOfDawnPos[9].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) - { - darion->AI()->Talk(SAY_LIGHT_OF_DAWN35); - darion->SetWalk(false); - } - break; - case EVENT_OUTRO_SCENE_11: - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - { - //darion->AI()->Talk(EMOTE_LIGHT_OF_DAWN07); - darion->GetMotionMaster()->MovePoint(0, LightOfDawnPos[10].GetPositionX(), LightOfDawnPos[10].GetPositionY(), LightOfDawnPos[10].GetPositionZ()); - } - break; - case EVENT_OUTRO_SCENE_12: - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - darion->AI()->Talk(EMOTE_LIGHT_OF_DAWN08); - break; - case EVENT_OUTRO_SCENE_13: - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - darion->AI()->Talk(SAY_LIGHT_OF_DAWN36); - break; - case EVENT_OUTRO_SCENE_14: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - alex->AI()->Talk(SAY_LIGHT_OF_DAWN37); - break; - case EVENT_OUTRO_SCENE_15: - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - darion->AI()->Talk(SAY_LIGHT_OF_DAWN38); - break; - case EVENT_OUTRO_SCENE_16: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - alex->AI()->Talk(SAY_LIGHT_OF_DAWN39); - - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->GetMotionMaster()->MovePoint(0, LightOfDawnPos[11].GetPositionX(), LightOfDawnPos[11].GetPositionY(), LightOfDawnPos[11].GetPositionZ()); - break; - case EVENT_OUTRO_SCENE_17: - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - darion->AI()->Talk(SAY_LIGHT_OF_DAWN40); - break; - case EVENT_OUTRO_SCENE_18: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - alex->AI()->Talk(SAY_LIGHT_OF_DAWN41); - - if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) - darion->DespawnOrUnsummon(3s); - break; - case EVENT_OUTRO_SCENE_19: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - alex->AI()->Talk(SAY_LIGHT_OF_DAWN42); - - events.Reset(); - events.ScheduleEvent(EVENT_OUTRO_SCENE_20, 4s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_21, 4s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_SCENE_22, 7s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_23, 9s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_24, 14s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_25, 21s + 200ms); - events.ScheduleEvent(EVENT_OUTRO_SCENE_26, 22s + 500ms); - events.ScheduleEvent(EVENT_OUTRO_SCENE_27, 24s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_28, 28s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_29, 34s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_30, 36s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_31, 51s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_32, 68s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_33, 73s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_34, 76s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_35, 77s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_36, 81s); - break; - case EVENT_OUTRO_SCENE_20: - if (Creature* lk = me->SummonCreature(NPC_THE_LICH_KING, LightOfDawnPos[12].GetPositionX(), LightOfDawnPos[12].GetPositionY(), LightOfDawnPos[12].GetPositionZ(), LightOfDawnPos[12].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN43); - break; - case EVENT_OUTRO_SCENE_21: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->CastSpell(lk, SPELL_ICEBOUND_VISAGE, true); - break; - case EVENT_OUTRO_SCENE_22: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - lk->AI()->Talk(SAY_LIGHT_OF_DAWN45); - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - { - alex->RemoveAllAuras(); - lk->CastSpell(alex, SPELL_SOUL_FEAST_ALEX, false); - } - } - break; - case EVENT_OUTRO_SCENE_23: - if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) - { - alex->DespawnOrUnsummon(5s); - alex->SetVisible(false); - } - break; - case EVENT_OUTRO_SCENE_24: - me->SetStandState(UNIT_STAND_STATE_STAND); - Talk(SAY_LIGHT_OF_DAWN44); - break; - case EVENT_OUTRO_SCENE_25: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->GetMotionMaster()->MovePoint(0, LightOfDawnPos[13].GetPositionX(), LightOfDawnPos[13].GetPositionY(), LightOfDawnPos[13].GetPositionZ()); - break; - case EVENT_OUTRO_SCENE_26: - me->CastSpell(me, SPELL_MOGRAINE_CHARGE, false); - break; - case EVENT_OUTRO_SCENE_27: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - lk->AI()->Talk(SAY_LIGHT_OF_DAWN46); - // Mograine's charge puts him inside the Lich King's collision, so we need to teleport him out - // Otherwise, Rebuke will kick him the wrong direction - me->NearTeleportTo(2279.7493f, -5258.1f, 80.065f, 4.3419204f); - lk->m_Events.AddEventAtOffset([&, lk] { - lk->CastSpell(me, SPELL_REBUKE, false); - }, 1s); - } - break; - case EVENT_OUTRO_SCENE_28: - me->SetStandState(UNIT_STAND_STATE_KNEEL); - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN47); - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - tirion->SetFacingToObject(lk); - } - break; - case EVENT_OUTRO_SCENE_29: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - lk->HandleEmoteCommand(EMOTE_ONESHOT_LAUGH); - lk->PlayDirectSound(14820); - } - break; - case EVENT_OUTRO_SCENE_30: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN48); - break; - case EVENT_OUTRO_SCENE_31: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN49); - break; - case EVENT_OUTRO_SCENE_32: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - lk->CastSpell(lk, SPELL_SOUL_FEAST_TIRION, false); - tirion->AI()->Talk(EMOTE_LIGHT_OF_DAWN12); - } - - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - if (summon->GetEntry() <= NPC_RIMBLAT_EARTHSHATTER && summon->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING) - { - float o = lk->GetAngle(summon); - summon->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE); - summon->GetMotionMaster()->MovePoint(3, lk->GetPositionX() + 2.0f * cos(o), lk->GetPositionY() + 2.0f * std::sin(o), lk->GetPositionZ()); - summon->ToTempSummon()->SetTempSummonType(TEMPSUMMON_MANUAL_DESPAWN); - } - } - break; - case EVENT_OUTRO_SCENE_33: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - lk->AI()->Talk(SAY_LIGHT_OF_DAWN51); - lk->CastSpell(lk, SPELL_APOCALYPSE, true); - } - break; - case EVENT_OUTRO_SCENE_34: - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - if (summon->GetEntry() <= NPC_RIMBLAT_EARTHSHATTER && summon->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING) - Unit::Kill(summon, summon); - break; - case EVENT_OUTRO_SCENE_35: - Talk(SAY_LIGHT_OF_DAWN52); - break; - case EVENT_OUTRO_SCENE_36: - me->SetStandState(UNIT_STAND_STATE_STAND); - Talk(SAY_LIGHT_OF_DAWN53); - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - me->SetFacingToObject(tirion); - - events.Reset(); - events.ScheduleEvent(EVENT_OUTRO_SCENE_37, 1s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_38, 5s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_39, 7s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_40, 9s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_41, 13s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_42, 16s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_43, 17s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_44, 19s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_45, 25s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_46, 32s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_47, 42s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_48, 52s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_49, 54s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_50, 58s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_51, 65s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_52, 70s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_53, 84s); - break; - case EVENT_OUTRO_SCENE_37: - me->SetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0, uint32(EQUIP_UNEQUIP)); - me->CastSpell(me, SPELL_THROW_ASHBRINGER, true); - break; - case EVENT_OUTRO_SCENE_38: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->RemoveAllAuras(); - tirion->CastSpell(me, SPELL_REBIRTH_OF_THE_ASHBRINGER, true); - tirion->SummonGameObject(GO_LIGHT_OF_DAWN, tirion->GetPositionX(), tirion->GetPositionY(), tirion->GetPositionZ(), tirion->GetOrientation(), 0, 0, 0, 0, 180); - tirion->LoadEquipment(1, true); - } - me->SetStandState(UNIT_STAND_STATE_DEAD); - break; - case EVENT_OUTRO_SCENE_39: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->RemoveAllAuras(); - tirion->HandleEmoteCommand(EMOTE_ONESHOT_ROAR); - } - break; - case EVENT_OUTRO_SCENE_40: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN54); - break; - case EVENT_OUTRO_SCENE_41: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN55); - break; - case EVENT_OUTRO_SCENE_42: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN56); - break; - case EVENT_OUTRO_SCENE_43: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->CastSpell(tirion, SPELL_TIRION_CHARGE, true); - tirion->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_READY2H); - tirion->SetImmuneToAll(true); - } - break; - case EVENT_OUTRO_SCENE_44: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN57); - break; - case EVENT_OUTRO_SCENE_45: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN58); - break; - case EVENT_OUTRO_SCENE_46: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - lk->AI()->Talk(SAY_LIGHT_OF_DAWN59); - break; - case EVENT_OUTRO_SCENE_47: - if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) - { - lk->CastSpell(lk, SPELL_EXIT_TELEPORT_VISUAL, true); - lk->DespawnOrUnsummon(1500ms); - } - - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - float o = me->GetAngle(tirion); - tirion->GetMotionMaster()->MovePoint(4, me->GetPositionX() + 2.0f * cos(o), me->GetPositionY() + 2.0f * std::sin(o), me->GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, false); - tirion->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); - tirion->SetFaction(FACTION_FRIENDLY); - } - break; - case EVENT_OUTRO_SCENE_48: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->CastSpell(me, SPELL_LAY_ON_HANDS, false); - me->SetStandState(UNIT_STAND_STATE_KNEEL); - break; - case EVENT_OUTRO_SCENE_49: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN60); - tirion->SetWalk(true); - } - break; - case EVENT_OUTRO_SCENE_50: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->GetMotionMaster()->MovePoint(4, LightOfDawnPos[14].GetPositionX(), LightOfDawnPos[14].GetPositionY(), LightOfDawnPos[14].GetPositionZ()); - break; - case EVENT_OUTRO_SCENE_51: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->GetMotionMaster()->MovePoint(4, LightOfDawnPos[15].GetPositionX(), LightOfDawnPos[15].GetPositionY(), LightOfDawnPos[15].GetPositionZ()); - break; - case EVENT_OUTRO_SCENE_52: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - { - tirion->SetFacingToObject(me); - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN61); - } - break; - case EVENT_OUTRO_SCENE_53: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN62); - - events.Reset(); - events.ScheduleEvent(EVENT_OUTRO_SCENE_54, 6s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_55, 14s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_56, 27s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_57, 37s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_58, 44s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_59, 50s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_60, 63s); - events.ScheduleEvent(EVENT_OUTRO_SCENE_61, 150s); - break; - case EVENT_OUTRO_SCENE_54: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN63); - break; - case EVENT_OUTRO_SCENE_55: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN64); - break; - case EVENT_OUTRO_SCENE_56: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN65); - break; - case EVENT_OUTRO_SCENE_57: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN66); - break; - case EVENT_OUTRO_SCENE_58: - if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) - tirion->AI()->Talk(SAY_LIGHT_OF_DAWN67); - break; - case EVENT_OUTRO_SCENE_59: - Talk(SAY_LIGHT_OF_DAWN68); - me->SetStandState(UNIT_STAND_STATE_STAND); - break; - case EVENT_OUTRO_SCENE_60: - { - Map::PlayerList const& PlayerList = me->GetMap()->GetPlayers(); - if (!PlayerList.IsEmpty()) - { - for (Map::PlayerList::const_iterator i = PlayerList.begin(); i != PlayerList.end(); ++i) - if (i->GetSource()->IsAlive() && me->IsWithinDistInMap(i->GetSource(), 100)) - i->GetSource()->CastSpell(i->GetSource(), SPELL_THE_LIGHT_OF_DAWN_Q, false); - } - me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_GOSSIP | UNIT_NPC_FLAG_QUESTGIVER); - break; - } - case EVENT_OUTRO_SCENE_61: - summons.DespawnAll(); - me->DespawnOrUnsummon(1ms); - events.Reset(); - return; - } - - if (battleStarted != ENCOUNTER_STATE_FIGHT) - return; - - if (!UpdateVictim()) - return; - - switch (eventId) - { - case EVENT_SPELL_ANTI_MAGIC_ZONE: - DoCast(me, SPELL_ANTI_MAGIC_ZONE1); - events.RescheduleEvent(eventId, 30s, 45s); - break; - case EVENT_SPELL_DEATH_STRIKE: - DoCastVictim(SPELL_DEATH_STRIKE); - events.RescheduleEvent(eventId, 5s, 35s); - break; - case EVENT_SPELL_DEATH_EMBRACE: - DoCastVictim(SPELL_DEATH_EMBRACE); - events.RescheduleEvent(eventId, 45s, 60s); - break; - case EVENT_SPELL_UNHOLY_BLIGHT: - DoCast(me, SPELL_UNHOLY_BLIGHT); - events.RescheduleEvent(eventId, 60s); - break; - case EVENT_SPELL_DARION_MOD_DAMAGE: - DoCast(me, SPELL_DARION_MOD_DAMAGE); - Talk(SAY_LIGHT_OF_DAWN09); - events.RescheduleEvent(eventId, 15s, 25s); - break; - } - - DoMeleeAttackIfReady(); - } - }; -}; - -class spell_chapter5_light_of_dawn_aura : public AuraScript -{ - PrepareAuraScript(spell_chapter5_light_of_dawn_aura); - - void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) - { - GetUnitOwner()->Dismount(); - GetUnitOwner()->SetCanFly(true); - GetUnitOwner()->SetDisableGravity(true); - GetUnitOwner()->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); - } - - void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) - { - GetUnitOwner()->SetCanFly(false); - GetUnitOwner()->SetDisableGravity(false); - GetUnitOwner()->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); - GetUnitOwner()->GetMotionMaster()->MoveFall(); - } - - void Register() override - { - OnEffectApply += AuraEffectApplyFn(spell_chapter5_light_of_dawn_aura::OnApply, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); - OnEffectRemove += AuraEffectRemoveFn(spell_chapter5_light_of_dawn_aura::OnRemove, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); - } -}; - -// 58552 - Return to Orgrimmar -// 58533 - Return to Stormwind -enum ReturnToCapital -{ - SPELL_RETURN_TO_ORGRIMMAR_APPLE = 58509, - SPELL_RETURN_TO_ORGRIMMAR_BANANA = 58513, - SPELL_RETURN_TO_ORGRIMMAR_SPIT = 58520, - - EMOTE_THROW_APPLE = 2, - EMOTE_THROW_BANANA = 3, - EMOTE_THROW_SPIT = 4, - SAY_INSULT_TO_DK = 5, - - NPC_SW_GUARD = 68, - NPC_ROYAL_GUARD = 1756, - NPC_CITY_PATROLLER = 1976, - NPC_OG_GUARD = 3296, - NPC_KOR_ELITE = 14304, - - TEXT_BROADCAST_COWER = 31670 // "%s cowers in fear." -}; - -uint32 ReturnToCapitalSpells[3] = -{ - 58509, // Apple - 58513, // Banana - 58520 // Spit -}; - -class spell_chapter5_return_to_capital : public SpellScript -{ - PrepareSpellScript(spell_chapter5_return_to_capital); - - bool Validate(SpellInfo const* /*spellInfo*/) override - { - return ValidateSpellInfo({ SPELL_RETURN_TO_ORGRIMMAR_APPLE, SPELL_RETURN_TO_ORGRIMMAR_BANANA, SPELL_RETURN_TO_ORGRIMMAR_SPIT}); - } - - void HandleHit(SpellEffIndex /*effIndex*/) - { - Creature* creature = GetHitUnit()->ToCreature(); - Player* player = GetCaster()->ToPlayer(); - uint32 spellId = GetSpellInfo()->Id; - - if (!spellId || !creature || !player || player->IsGameMaster() || !player->IsAlive() || !creature->IsAlive() || creature->IsInCombat()) - return; - - if (creature->HasSpellCooldown(spellId)) - return; - - if (creature->GetEntry() == NPC_SW_GUARD || creature->GetEntry() == NPC_ROYAL_GUARD || creature->GetEntry() == NPC_CITY_PATROLLER || creature->GetEntry() == NPC_OG_GUARD || creature->GetEntry() == NPC_KOR_ELITE) - { - _emote = urand(2,4); - if (creature) - { - creature->PauseMovement(5000); - creature->SetFacingToObject(player, 30s); - - if (roll_chance_i(30)) - { - creature->AI()->Talk(_emote, player); - creature->CastSpell(player, ReturnToCapitalSpells[_emote - 2]); - } - else - { - creature->AI()->Talk(SAY_INSULT_TO_DK, player); - creature->HandleEmoteCommand(RAND(EMOTE_ONESHOT_POINT,EMOTE_ONESHOT_RUDE)); - } - } - } - /*/// @todo: This needs to be further investigated as there are some "guard" npcs, that have civilian flags and non guard npcs should also insult the dk. - else - if (creature->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_CIVILIAN) - { - creature->HandleEmoteCommand(EMOTE_STATE_COWER); // from sniff, emote 431 for a while, then reset (with "%s cowers in fear." text) - creature->PlayDirectSound(14556); // from sniff - if (player) - { - LocaleConstant loc_idx = player->GetSession()->GetSessionDbLocaleIndex(); - if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(TEXT_BROADCAST_COWER)) - creature->TextEmote(bct->GetText(loc_idx, creature->getGender()), creature); - } - } - */ - - creature->AddSpellCooldown(spellId, 0, 30000); - } - - void Register() override - { - OnEffectHitTarget += SpellEffectFn(spell_chapter5_return_to_capital::HandleHit, EFFECT_0, SPELL_EFFECT_DUMMY); - } -private: - uint8 _emote; -}; - -void AddSC_the_scarlet_enclave_c5() -{ - new npc_highlord_darion_mograine(); - RegisterSpellScript(spell_chapter5_light_of_dawn_aura); - RegisterSpellScript(spell_chapter5_return_to_capital); -} diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/zone_the_scarlet_enclave.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/zone_the_scarlet_enclave.cpp deleted file mode 100644 index 219911e74..000000000 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/zone_the_scarlet_enclave.cpp +++ /dev/null @@ -1,143 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#include "CreatureScript.h" -#include "PassiveAI.h" -#include "Player.h" -#include "ScriptedCreature.h" - -/*#### -## npc_valkyr_battle_maiden -####*/ - -enum Spells_VBM -{ - SPELL_REVIVE = 51918 -}; - -enum Says_VBM -{ - WHISPER_REVIVE = 0 -}; - -class npc_valkyr_battle_maiden : public CreatureScript -{ -public: - npc_valkyr_battle_maiden() : CreatureScript("npc_valkyr_battle_maiden") { } - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_valkyr_battle_maidenAI(creature); - } - - struct npc_valkyr_battle_maidenAI : public PassiveAI - { - npc_valkyr_battle_maidenAI(Creature* creature) : PassiveAI(creature) { } - - uint32 FlyBackTimer; - float x, y, z; - uint32 phase; - - void Reset() override - { - me->setActive(true); - me->SetVisible(false); - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); - me->SetCanFly(true); - FlyBackTimer = 500; - phase = 0; - - me->GetPosition(x, y, z); - z += 4.0f; - x -= 3.5f; - y -= 5.0f; - me->GetMotionMaster()->Clear(false); - me->SetPosition(x, y, z, 0.0f); - } - - void UpdateAI(uint32 diff) override - { - if (FlyBackTimer <= diff) - { - Player* player = nullptr; - if (me->IsSummon()) - { - if (Unit * summoner = me->ToTempSummon()->GetSummonerUnit()) - { - player = summoner->ToPlayer(); - } - } - - if (!player) - { - phase = 3; - } - - switch (phase) - { - case 0: - me->SetWalk(false); - me->HandleEmoteCommand(EMOTE_STATE_FLYGRABCLOSED); - FlyBackTimer = 500; - break; - case 1: - if (player) - { - player->GetClosePoint(x, y, z, me->GetObjectSize()); - } - z += 2.5f; - x -= 2.0f; - y -= 1.5f; - me->GetMotionMaster()->MovePoint(0, x, y, z); - if (player) - { - me->SetTarget(player->GetGUID()); - } - me->SetVisible(true); - FlyBackTimer = 4500; - break; - case 2: - if (player && !player->isResurrectRequested()) - { - me->HandleEmoteCommand(EMOTE_ONESHOT_CUSTOM_SPELL_01); - DoCast(player, SPELL_REVIVE, true); - Talk(WHISPER_REVIVE, player); - } - FlyBackTimer = 5000; - break; - case 3: - me->SetVisible(false); - FlyBackTimer = 3000; - break; - case 4: - me->DisappearAndDie(); - break; - default: - //Nothing To DO - break; - } - ++phase; - } - else FlyBackTimer -= diff; - } - }; -}; - -void AddSC_the_scarlet_enclave() -{ - new npc_valkyr_battle_maiden(); -} diff --git a/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp b/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp index 9555f853e..897d168e9 100644 --- a/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp +++ b/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp @@ -97,10 +97,6 @@ void AddSC_boss_majordomo(); void AddSC_boss_ragnaros(); void AddSC_instance_molten_core(); void AddSC_the_scarlet_enclave(); //Scarlet Enclave -void AddSC_the_scarlet_enclave_c1(); -void AddSC_the_scarlet_enclave_c2(); -void AddSC_the_scarlet_enclave_c3(); -void AddSC_the_scarlet_enclave_c5(); void AddSC_instance_scarlet_monastery(); //Scarlet Monastery void AddSC_boss_kirtonos_the_herald(); void AddSC_boss_darkmaster_gandling(); @@ -249,10 +245,6 @@ void AddEasternKingdomsScripts() AddSC_boss_ragnaros(); AddSC_instance_molten_core(); AddSC_the_scarlet_enclave(); //Scarlet Enclave - AddSC_the_scarlet_enclave_c1(); - AddSC_the_scarlet_enclave_c2(); - AddSC_the_scarlet_enclave_c3(); - AddSC_the_scarlet_enclave_c5(); AddSC_instance_scarlet_monastery(); //Scarlet Monastery AddSC_boss_kirtonos_the_herald(); AddSC_boss_darkmaster_gandling(); diff --git a/src/server/scripts/EasternKingdoms/zone_the_scarlet_enclave.cpp b/src/server/scripts/EasternKingdoms/zone_the_scarlet_enclave.cpp new file mode 100644 index 000000000..27a9ac07a --- /dev/null +++ b/src/server/scripts/EasternKingdoms/zone_the_scarlet_enclave.cpp @@ -0,0 +1,2945 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "CreatureScript.h" +#include "PassiveAI.h" +#include "Player.h" +#include "ScriptedCreature.h" +#include "CombatAI.h" +#include "CreatureTextMgr.h" +#include "GameObjectScript.h" +#include "MoveSplineInit.h" +#include "ObjectMgr.h" +#include "ScriptedEscortAI.h" +#include "ScriptedGossip.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "SpellScriptLoader.h" +#include "ObjectAccessor.h" +#include "SpellAuras.h" +#include +#include "WorldStateDefines.h" + +/*###### + ## Quest: Death Comes From High + ## Note: Recall Eye of Acherus and Summon Ghouls On Scarlet Crusade (Spell Scripts) + ######*/ + +enum DeathComesFromOnHigh +{ + SPELL_THE_EYE_OF_ACHERUS = 51852, + SPELL_CALL_OF_THE_DEAD = 51900 +}; + +// 51904 - Summon Ghouls On Scarlet Crusade +class spell_q12641_death_comes_from_on_high_summon_ghouls : public SpellScript +{ + PrepareSpellScript(spell_q12641_death_comes_from_on_high_summon_ghouls); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_CALL_OF_THE_DEAD }); + } + + void HandleScriptEffect(SpellEffIndex effIndex) + { + PreventHitEffect(effIndex); + if (Unit* target = GetHitUnit()) + target->CastSpell(target, SPELL_CALL_OF_THE_DEAD, true); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_q12641_death_comes_from_on_high_summon_ghouls::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 52694 - Recall Eye of Acherus +class spell_q12641_death_comes_from_on_high_recall_eye : public SpellScript +{ + PrepareSpellScript(spell_q12641_death_comes_from_on_high_recall_eye); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_THE_EYE_OF_ACHERUS }); + } + + void HandleScriptEffect(SpellEffIndex effIndex) + { + PreventHitEffect(effIndex); + Unit* caster = GetCaster(); + Unit* owner = caster->GetCharmerOrOwner(); + + if (!caster || !owner) + return; + + if (owner->HasAura(SPELL_THE_EYE_OF_ACHERUS)) + owner->RemoveAurasDueToSpell(SPELL_THE_EYE_OF_ACHERUS); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_q12641_death_comes_from_on_high_recall_eye::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +/*###### +## Rain of Darkness +## Quest: If Chaos Drives, Let Suffering Hold The Reins. +## Note: Increase Rain of Darkness (Trigger) Z Axis Spawn Point. +######*/ + +// 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); + } +}; + +/*###### + ## Quest: Gift Of The Harvester + ######*/ + +enum GiftOfTheHarvester +{ + NPC_GHOUL = 28845, + MAX_GHOULS = 5, + + SPELL_GHOUL_EMERGE = 50142, + SPELL_SUMMON_SCARLET_GHOST = 52505, + SPELL_GHOUL_SUBMERGE = 26234, + + EVENT_GHOUL_RESTORE_STATE = 1, + EVENT_GHOUL_CHECK_COMBAT = 2, + EVENT_GHOUL_EMOTE = 3, + EVENT_GHOUL_MOVE_TO_PIT = 4, + + SAY_GOTHIK_PIT = 0, + + ACTION_DK_INITIATE_ASSAULT_ROAR = 15 + +}; + +class spell_item_gift_of_the_harvester : public SpellScript +{ + PrepareSpellScript(spell_item_gift_of_the_harvester); + + SpellCastResult CheckRequirement() + { + std::list ghouls; + GetCaster()->GetAllMinionsByEntry(ghouls, NPC_GHOUL); + if (ghouls.size() >= MAX_GHOULS) + { + SetCustomCastResultMessage(SPELL_CUSTOM_ERROR_TOO_MANY_GHOULS); + return SPELL_FAILED_CUSTOM_ERROR; + } + + return SPELL_CAST_OK; + } + + void Register() override + { + OnCheckCast += SpellCheckCastFn(spell_item_gift_of_the_harvester::CheckRequirement); + } +}; + +class spell_q12698_the_gift_that_keeps_on_giving : public SpellScript +{ + PrepareSpellScript(spell_q12698_the_gift_that_keeps_on_giving); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_SUMMON_SCARLET_GHOST }); + } + + void HandleScriptEffect(SpellEffIndex /*effIndex*/) + { + if (GetOriginalCaster() && GetHitUnit()) + GetOriginalCaster()->CastSpell(GetHitUnit(), urand(0, 1) ? GetEffectValue() : SPELL_SUMMON_SCARLET_GHOST, true); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_q12698_the_gift_that_keeps_on_giving::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +struct npc_scarlet_ghoul : public ScriptedAI +{ + npc_scarlet_ghoul(Creature* creature) : ScriptedAI(creature) + { + } + + EventMap events; + ObjectGuid gothikGUID; + + void InitializeAI() override + { + me->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE); + ScriptedAI::InitializeAI(); + me->SetReactState(REACT_PASSIVE); + + events.ScheduleEvent(EVENT_GHOUL_EMOTE, 1ms); + events.ScheduleEvent(EVENT_GHOUL_RESTORE_STATE, 3500ms); + } + + void OwnerAttackedBy(Unit* attacker) override + { + if (!me->IsInCombat() && me->GetReactState() == REACT_DEFENSIVE) + AttackStart(attacker); + } + + void SetGUID(ObjectGuid const& guid, int32) override + { + gothikGUID = guid; + events.ScheduleEvent(EVENT_GHOUL_MOVE_TO_PIT, 3s); + me->GetMotionMaster()->Clear(false); + } + + void MovementInform(uint32 type, uint32 point) override + { + if (type == POINT_MOTION_TYPE && point == 1) + { + me->DespawnOrUnsummon(1500ms); + me->CastSpell(me, SPELL_GHOUL_SUBMERGE, true); + } + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + switch (events.ExecuteEvent()) + { + case EVENT_GHOUL_MOVE_TO_PIT: + me->GetMotionMaster()->MovePoint(1, 2364.77f, -5776.14f, 151.36f); + if (Creature* gothik = ObjectAccessor::GetCreature(*me, gothikGUID)) + gothik->AI()->DoAction(SAY_GOTHIK_PIT); + break; + case EVENT_GHOUL_EMOTE: + me->CastSpell(me, SPELL_GHOUL_EMERGE, true); + break; + case EVENT_GHOUL_RESTORE_STATE: + me->SetReactState(REACT_DEFENSIVE); + me->RemoveUnitFlag(UNIT_FLAG_DISABLE_MOVE); + if (Player* owner = me->GetCharmerOrOwnerPlayerOrPlayerItself()) + me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, frand(0.0f, 2 * M_PI)); + events.ScheduleEvent(EVENT_GHOUL_CHECK_COMBAT, 1s); + return; + case EVENT_GHOUL_CHECK_COMBAT: + if (!me->IsInCombat()) + if (Player* owner = me->GetCharmerOrOwnerPlayerOrPlayerItself()) + if (owner->GetVictim()) + AttackStart(owner->GetVictim()); + + events.Repeat(1s); + return; + } + + if (!UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } +}; + +struct npc_dkc1_gothik : public ScriptedAI +{ + npc_dkc1_gothik(Creature* creature) : ScriptedAI(creature) { spoken = 0; } + + int32 spoken; + + void DoAction(int32 action) override + { + if (action == SAY_GOTHIK_PIT && spoken <= 0) + { + spoken = 5000; + Talk(SAY_GOTHIK_PIT); + } + + if (action == ACTION_DK_INITIATE_ASSAULT_ROAR) + me->HandleEmoteCommand(EMOTE_ONESHOT_ROAR); + } + + void MoveInLineOfSight(Unit* who) override + { + ScriptedAI::MoveInLineOfSight(who); + + if (!who->IsImmuneToNPC() && who->GetEntry() == NPC_GHOUL && me->IsWithinDistInMap(who, 10.0f)) + if (Unit* owner = who->GetOwner()) + if (Player* player = owner->ToPlayer()) + { + Creature* creature = who->ToCreature(); + if (player->GetQuestStatus(12698) == QUEST_STATUS_INCOMPLETE) + creature->CastSpell(owner, 52517, true); + + creature->AI()->SetGUID(me->GetGUID()); + creature->SetImmuneToAll(true); + } + } + + void UpdateAI(uint32 diff) override + { + if (spoken > 0) + spoken -= diff; + + ScriptedAI::UpdateAI(diff); + } +}; + +/*###### +## Death Knight Initiate Visual Spell +## Quest: The Endless Hunger +## Note: Trigger Based On Creature Race +######*/ + +#define GCD_CAST 1 + +enum UnworthyInitiate +{ + SPELL_DK_INITIATE_VISUAL = 51519, +}; + +class spell_death_knight_initiate_visual : public SpellScript +{ + PrepareSpellScript(spell_death_knight_initiate_visual); + + void HandleScriptEffect(SpellEffIndex /* effIndex */) + { + Creature* target = GetHitCreature(); + if (!target) + return; + + uint32 spellId; + switch (target->GetDisplayId()) + { + case 25369: spellId = 51552; break; // bloodelf female + case 25373: spellId = 51551; break; // bloodelf male + case 25363: spellId = 51542; break; // draenei female + case 25357: spellId = 51541; break; // draenei male + case 25361: spellId = 51537; break; // dwarf female + case 25356: spellId = 51538; break; // dwarf male + case 25372: spellId = 51550; break; // forsaken female + case 25367: spellId = 51549; break; // forsaken male + case 25362: spellId = 51540; break; // gnome female + case 25359: spellId = 51539; break; // gnome male + case 25355: spellId = 51534; break; // human female + case 25354: spellId = 51520; break; // human male + case 25360: spellId = 51536; break; // nightelf female + case 25358: spellId = 51535; break; // nightelf male + case 25368: spellId = 51544; break; // orc female + case 25364: spellId = 51543; break; // orc male + case 25371: spellId = 51548; break; // tauren female + case 25366: spellId = 51547; break; // tauren male + case 25370: spellId = 51545; break; // troll female + case 25365: spellId = 51546; break; // troll male + default: return; + } + + target->CastSpell(target, spellId, true); + target->LoadEquipment(); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_death_knight_initiate_visual::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +/*###### +## Lich King Whisper +## Quest: If Chaos Drives, Let Suffering Hold The Reins. +######*/ + +enum spells_lich_king_whisper +{ + SPELL_LICH_KING_VO_BLOCKER = 58207, + SPELL_LICHKINGDK001 = 58208, + SPELL_LICHKINGDK002 = 58209, + SPELL_LICHKINGDK003 = 58210, + SPELL_LICHKINGDK004 = 58211, + SPELL_LICHKINGDK005 = 58212, + SPELL_LICHKINGDK006 = 58213, + SPELL_LICHKINGDK007 = 58214, + SPELL_LICHKINGDK008 = 58215, + SPELL_LICHKINGDK009 = 58216, + SPELL_LICHKINGDK010 = 58217, + SPELL_LICHKINGDK011 = 58218, + SPELL_LICHKINGDK012 = 58219, + SPELL_LICHKINGDK013 = 58220, + SPELL_LICHKINGDK014 = 58221, + SPELL_LICHKINGDK015 = 58222, + SPELL_LICHKINGDK016 = 58223 +}; + +//spell 58207 rand Whisper +class spell_lich_king_vo_blocker : public AuraScript +{ + PrepareAuraScript(spell_lich_king_vo_blocker); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo + ({ + SPELL_LICHKINGDK001, SPELL_LICHKINGDK002, SPELL_LICHKINGDK003, SPELL_LICHKINGDK004, + SPELL_LICHKINGDK005, SPELL_LICHKINGDK006, SPELL_LICHKINGDK007, SPELL_LICHKINGDK008, + SPELL_LICHKINGDK009, SPELL_LICHKINGDK010, SPELL_LICHKINGDK011, SPELL_LICHKINGDK012, + SPELL_LICHKINGDK013, SPELL_LICHKINGDK014, SPELL_LICHKINGDK015, SPELL_LICHKINGDK016 + }); + } + + void HandleEffectApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) + { + if (Player* target = GetTarget()->ToPlayer()) + { + //spell 58208-58223 + GetCaster()->CastSpell(target, urand(SPELL_LICHKINGDK001, SPELL_LICHKINGDK016), true); + } + } + + void Register() override + { + OnEffectApply += AuraEffectApplyFn(spell_lich_king_vo_blocker::HandleEffectApply, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); + } +}; + +// 58208 - 58224 - Creature - The Lich King (28765) Whisper +class spell_lich_king_whisper : public SpellScript +{ + PrepareSpellScript(spell_lich_king_whisper); + + bool Validate(SpellInfo const* spellInfo) override + { + return sObjectMgr->GetBroadcastText(uint32(spellInfo->GetEffect(EFFECT_0).CalcValue())) && + sSoundEntriesStore.LookupEntry(uint32(spellInfo->GetEffect(EFFECT_1).CalcValue())); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + if (Player* player = GetHitPlayer()) + GetCaster()->Whisper(uint32(GetEffectValue()), player, false); + } + + void HandleDummy(SpellEffIndex /*effIndex*/) + { + if (Player* player = GetHitPlayer()) + player->PlayDistanceSound(uint32(GetEffectValue()), player); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_lich_king_whisper::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + OnEffectHitTarget += SpellEffectFn(spell_lich_king_whisper::HandleDummy, EFFECT_1, SPELL_EFFECT_DUMMY); + } +}; + +/*###### +## Npc Koltira Deathweaver +## Quest: Bloody Breakout +######*/ + +enum Koltira +{ + SAY_BREAKOUT0 = 0, + SAY_BREAKOUT1 = 1, + SAY_BREAKOUT2 = 2, + SAY_BREAKOUT3 = 3, + SAY_BREAKOUT4 = 4, + SAY_BREAKOUT5 = 5, + SAY_BREAKOUT6 = 6, + SAY_BREAKOUT7 = 7, + SAY_BREAKOUT8 = 8, + SAY_BREAKOUT9 = 9, + SAY_BREAKOUT10 = 10, + EMOTE_KOLTIRA_COLLAPSES = 11, + + SAY_VALROTH_WAVE3 = 0, + SAY_VALROTH_AGGRO = 1, + SAY_VALROTH_WAVE1 = 4, + SAY_VALROTH_WAVE2 = 5, + + SPELL_KOLTIRA_TRANSFORM = 52899, + SPELL_ANTI_MAGIC_ZONE = 52894, + + QUEST_BREAKOUT = 12727, + + NPC_CRIMSON_ACOLYTE = 29007, + NPC_HIGH_INQUISITOR_VALROTH = 29001, + + //not sure about this id + //NPC_DEATH_KNIGHT_MOUNT = 29201, + MODEL_DEATH_KNIGHT_MOUNT = 25278, + + POINT_STAND_UP = 1, + POINT_BOX = 2, + POINT_ANTI_MAGIC_ZONE = 3, + + POINT_MOUNT = 1, + POINT_DESPAWN = 2 +}; + +struct npc_koltira_deathweaver : public ScriptedAI +{ + npc_koltira_deathweaver(Creature* creature) : ScriptedAI(creature) { } + + void Reset() override + { + scheduler.CancelAll(); + me->m_Events.KillAllEvents(false); + me->SetUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); + me->setActive(false); + SetEquipmentSlots(true); + } + + void StartEvent() + { + if (!me->HasNpcFlag(UNIT_NPC_FLAG_GOSSIP)) // Already in progress + return; + + me->SetStandState(UNIT_STAND_STATE_SIT); + me->RemoveNpcFlag(UNIT_NPC_FLAG_GOSSIP); + me->setActive(true); + + Talk(SAY_BREAKOUT0); + + me->m_Events.AddEventAtOffset([&] { + me->GetMotionMaster()->MoveWaypoint(me->GetEntry() * 10, false); + }, 5s); + } + + void sQuestAccept(Player* /*player*/, Quest const* quest) override + { + if (quest->GetQuestId() == QUEST_BREAKOUT) + StartEvent(); + } + + void sGossipSelect(Player* player, uint32 /*menuId*/, uint32 /*gossipListId*/) override + { + if (player->GetQuestStatus(QUEST_BREAKOUT) == QUEST_STATUS_INCOMPLETE) + { + CloseGossipMenuFor(player); + StartEvent(); + } + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != WAYPOINT_MOTION_TYPE) + return; + + if (!me->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC)) + { + if (id == POINT_MOUNT) + me->Mount(MODEL_DEATH_KNIGHT_MOUNT); + else if (id == POINT_DESPAWN) + { + me->Dismount(); + me->DespawnOrUnsummon(); + } + + return; + } + + switch (id) + { + case POINT_STAND_UP: + Talk(SAY_BREAKOUT1); + break; + case POINT_BOX: + me->SetStandState(UNIT_STAND_STATE_KNEEL); + + scheduler.Schedule(5s, [this](TaskContext context) + { + switch (context.GetRepeatCounter()) + { + case 0: + Talk(SAY_BREAKOUT3); + + // Shouldn't actually be spawned at this point, but no way to send his yells otherwise? + if (Creature* valroth = me->SummonCreature(NPC_HIGH_INQUISITOR_VALROTH, 1640.8596f, -6030.834f, 134.82211f, 4.606426715850830078f, TEMPSUMMON_MANUAL_DESPAWN)) + { + _valrothGUID = valroth->GetGUID(); + valroth->AI()->Talk(SAY_VALROTH_WAVE1); + valroth->SetReactState(REACT_PASSIVE); + } + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.6724f, -6032.0527f, 134.82213f, 4.654973506927490234f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint(NPC_CRIMSON_ACOLYTE * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.0055f, -6031.893f, 134.82211f, 0.401425719261169433f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 1) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1639.7053f, -6031.7373f, 134.82213f, 2.443460941314697265f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 2) * 10, false); + break; + case 1: + Talk(SAY_BREAKOUT4); + + if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) + valroth->AI()->Talk(SAY_VALROTH_WAVE2); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.7958f, -6030.307f, 134.82211f, 4.65355682373046875f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 3) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.7305f, -6030.751f, 134.82211f, 6.143558979034423828f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 4) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1639.4657f, -6030.404f, 134.82211f, 4.502949237823486328f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 5) * 10, false); + break; + case 2: + Talk(SAY_BREAKOUT5); + + if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) + valroth->AI()->Talk(SAY_VALROTH_WAVE3); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1641.3405f, -6031.436f, 134.82211f, 4.612849712371826171f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 6) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1642.0404f, -6030.3843f, 134.82211f, 1.378810048103332519f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 7) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.1162f, -6029.7817f, 134.82211f, 5.707226753234863281f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 8) * 10, false); + + if (Creature* acolyte = me->SummonCreature(NPC_CRIMSON_ACOLYTE, 1640.9948f, -6029.8027f, 134.82211f, 1.605702877044677734f, TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 15000)) + acolyte->GetMotionMaster()->MoveWaypoint((NPC_CRIMSON_ACOLYTE + 9) * 10, false); + break; + case 3: + Talk(SAY_BREAKOUT6); + me->m_Events.AddEventAtOffset([this] + { + Talk(EMOTE_KOLTIRA_COLLAPSES, me); + me->KillSelf(); + + if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) + valroth->DespawnOrUnsummon(); + }, 2min); + + if (Creature* valroth = ObjectAccessor::GetCreature(*me, _valrothGUID)) + { + valroth->AI()->Talk(SAY_VALROTH_AGGRO); + valroth->SetReactState(REACT_AGGRESSIVE); + valroth->GetMotionMaster()->MoveWaypoint(NPC_HIGH_INQUISITOR_VALROTH * 10, false); + } + return; + default: + break; + } + + context.Repeat(20s); + }); + + scheduler.Schedule(3s, [this](TaskContext) + { + DoCastSelf(SPELL_KOLTIRA_TRANSFORM); + me->LoadEquipment(); + }); + break; + case POINT_ANTI_MAGIC_ZONE: + me->SetStandState(UNIT_STAND_STATE_KNEEL); + Talk(SAY_BREAKOUT2); + DoCastSelf(SPELL_ANTI_MAGIC_ZONE); + break; + default: + break; + } + } + + void SummonedCreatureDies(Creature* summon, Unit*) override + { + if (summon->GetEntry() == NPC_HIGH_INQUISITOR_VALROTH) + { + me->m_Events.KillAllEvents(false); + me->RemoveAurasDueToSpell(SPELL_ANTI_MAGIC_ZONE); + me->SetStandState(UNIT_STAND_STATE_STAND); + Talk(SAY_BREAKOUT8, 3s); + Talk(SAY_BREAKOUT9, 8s); + scheduler.Schedule(11s, [this](TaskContext) + { + Talk(SAY_BREAKOUT10); + SetInvincibility(true); + me->SetReactState(REACT_PASSIVE); + me->RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); + me->GetMotionMaster()->MoveWaypoint((me->GetEntry() + 1) * 10, false); + }); + } + } + + void UpdateAI(uint32 diff) override + { + scheduler.Update(diff); + } + +private: + ObjectGuid _valrothGUID; +}; + +/*###### +## Npc Scarlet Courier +## Quest: Ambush At The Overlook +######*/ + +enum ScarletCourierEnum +{ + SAY_TREE1 = 0, + SAY_TREE2 = 1, + SPELL_SHOOT = 52818, + GO_INCONSPICUOUS_TREE = 191144, + NPC_SCARLET_COURIER = 29076 +}; + +struct npc_scarlet_courier : public ScriptedAI +{ + npc_scarlet_courier(Creature* creature) : ScriptedAI(creature) { } + + uint32 uiStage; + uint32 uiStage_timer; + + void Reset() override + { + me->Mount(14338); // not sure about this id + uiStage = 1; + uiStage_timer = 3000; + } + + void JustEngagedWith(Unit* /*who*/) override + { + Talk(SAY_TREE2); + me->Dismount(); + uiStage = 0; + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + if (id == 1) + uiStage = 2; + } + + void UpdateAI(uint32 diff) override + { + if (uiStage && !me->IsInCombat()) + { + if (uiStage_timer <= diff) + { + switch (uiStage) + { + case 1: + me->SetWalk(true); + if (GameObject* tree = me->FindNearestGameObject(GO_INCONSPICUOUS_TREE, 40.0f)) + { + Talk(SAY_TREE1); + float x, y, z; + tree->GetContactPoint(me, x, y, z); + me->GetMotionMaster()->MovePoint(1, x, y, z); + } + break; + case 2: + if (GameObject* tree = me->FindNearestGameObject(GO_INCONSPICUOUS_TREE, 40.0f)) + if (Unit* unit = tree->GetOwner()) + AttackStart(unit); + break; + } + uiStage_timer = 3000; + uiStage = 0; + } + else uiStage_timer -= diff; + } + + if (!UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } +}; + +/*###### +## Various NPCs AI +## Quest: A Special Surprise +## Note: Used by 29032, 29061, 29065, 29067, 29068, 29070, 29074, 29072, 29073, 29071 but signed for 29032 +######*/ + +enum SpecialSurprise +{ + SAY_EXEC_START = 0, + SAY_EXEC_PROG = 1, + SAY_EXEC_NAME = 2, + SAY_EXEC_RECOG = 3, + SAY_EXEC_NOREM = 4, + SAY_EXEC_THINK = 5, + SAY_EXEC_LISTEN = 6, + SAY_EXEC_TIME = 7, + SAY_EXEC_WAITING = 8, + EMOTE_DIES = 9, + + SAY_PLAGUEFIST = 0, + NPC_PLAGUEFIST = 29053 +}; + +struct npc_a_special_surprise : public ScriptedAI +{ + npc_a_special_surprise(Creature* creature) : ScriptedAI(creature) { } + + uint32 ExecuteSpeech_Timer; + uint32 ExecuteSpeech_Counter; + ObjectGuid PlayerGUID; + + void Reset() override + { + ExecuteSpeech_Timer = 0; + ExecuteSpeech_Counter = 0; + PlayerGUID.Clear(); + + me->SetReactState(REACT_PASSIVE); + me->SetImmuneToPC(true); + } + + bool MeetQuestCondition(Player* player) + { + switch (me->GetEntry()) + { + case 29061: // Ellen Stanbridge + if (player->GetQuestStatus(12742) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29072: // Kug Ironjaw + if (player->GetQuestStatus(12748) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29067: // Donovan Pulfrost + if (player->GetQuestStatus(12744) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29065: // Yazmina Oakenthorn + if (player->GetQuestStatus(12743) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29071: // Antoine Brack + if (player->GetQuestStatus(12750) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29032: // Malar Bravehorn + if (player->GetQuestStatus(12739) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29068: // Goby Blastenheimer + if (player->GetQuestStatus(12745) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29073: // Iggy Darktusk + if (player->GetQuestStatus(12749) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29074: // Lady Eonys + if (player->GetQuestStatus(12747) == QUEST_STATUS_INCOMPLETE) + return true; + break; + case 29070: // Valok the Righteous + if (player->GetQuestStatus(12746) == QUEST_STATUS_INCOMPLETE) + return true; + break; + } + + return false; + } + + void MoveInLineOfSight(Unit* who) override + + { + if (PlayerGUID || !who->IsPlayer() || !who->IsWithinDist(me, INTERACTION_DISTANCE)) + return; + + if (MeetQuestCondition(who->ToPlayer())) + PlayerGUID = who->GetGUID(); + } + + void UpdateAI(uint32 diff) override + { + if (PlayerGUID && !me->GetVictim() && me->IsAlive()) + { + if (ExecuteSpeech_Timer <= diff) + { + Player* player = ObjectAccessor::GetPlayer(*me, PlayerGUID); + + if (!player) + { + Reset(); + return; + } + + switch (ExecuteSpeech_Counter) + { + case 0: + Talk(SAY_EXEC_START, player); + break; + case 1: + me->SetStandState(UNIT_STAND_STATE_STAND); + break; + case 2: + Talk(SAY_EXEC_PROG, player); + break; + case 3: + Talk(SAY_EXEC_NAME, player); + break; + case 4: + Talk(SAY_EXEC_RECOG, player); + break; + case 5: + Talk(SAY_EXEC_NOREM, player); + break; + case 6: + Talk(SAY_EXEC_THINK, player); + break; + case 7: + Talk(SAY_EXEC_LISTEN, player); + break; + case 8: + if (Creature* Plaguefist = GetClosestCreatureWithEntry(me, NPC_PLAGUEFIST, 85.0f)) + { + Plaguefist->AI()->Talk(SAY_PLAGUEFIST, player); + } + break; + case 9: + Talk(SAY_EXEC_TIME, player); + me->SetStandState(UNIT_STAND_STATE_KNEEL); + me->SetReactState(REACT_PASSIVE); + me->SetImmuneToPC(false); + break; + case 10: + Talk(SAY_EXEC_WAITING, player); + break; + case 11: + Talk(EMOTE_DIES); + me->setDeathState(DeathState::JustDied); + me->SetHealth(0); + return; + } + + if (ExecuteSpeech_Counter >= 9) + ExecuteSpeech_Timer = 15000; + else + ExecuteSpeech_Timer = 7000; + + ++ExecuteSpeech_Counter; + } + else + ExecuteSpeech_Timer -= diff; + } + } +}; + +/*###### +## Gothik the Harvester and Acherus Necromancer AI +## Phase 4 Internal Mechanics. +######*/ + +enum NecroSpells +{ + SPELL_SCARLET_GHOUL = 52683, // Raises a Scarlet Ghoul from a humanoid corpse + SPELL_SCOURGE_GRYPHON = 52685, // Raises a Scourge Gryphon from a gryphon corpse + SPELL_GHOULPLOSION = 52672 // Causes a Gluttonous Geist to explode (kill) +}; + +enum NecroNPCs +{ + NPC_GLUTTONOUS_GEIST = 28905, + NPC_DEAD_SCARLET_MEDIC = 28895, + NPC_DEAD_SCARLET_INFANTRYMAN = 28896, + NPC_DEAD_SCARLET_CAPTAIN = 28898, + NPC_DEAD_SCARLET_PEASANT = 28892, + NPC_DEAD_SCARLET_MINER = 28891, + NPC_DEAD_SCARLET_FLEET_DEFENDER = 28886, + NPC_DEAD_SCARLET_GRYPHON = 28893 +}; + +struct npc_acherus_necromancer : public ScriptedAI +{ + npc_acherus_necromancer(Creature* creature) : ScriptedAI(creature) { } + + EventMap events; + ObjectGuid targetCorpseGUID; + ObjectGuid geistGUID; + bool isOnRitual; + + // Event timers (IDs) + enum Events + { + EVENT_START_RITUAL = 1, + EVENT_GHOULPLOSION, + EVENT_RAISE_GHOUL, + EVENT_RESUME_WP + }; + + // Point ID for movement + enum Points + { + POINT_CORPSE_REACHED = 1 + }; + + void Reset() override + { + events.Reset(); + targetCorpseGUID.Clear(); + geistGUID.Clear(); + isOnRitual = false; + // Start waypoint movement using WaypointMovementGenerator + if (uint32 pathId = me->GetWaypointPath()) + { + me->GetMotionMaster()->MoveWaypoint(pathId, true); // true = repeatable + } + // Schedule the first ritual after 20-30s + events.ScheduleEvent(EVENT_START_RITUAL, 20s, 30s); + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + + if (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_RITUAL: + { + if (isOnRitual) // Already performing ritual + { + events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); + break; + } + + // Find nearest dead Scarlet humanoid (exclude gryphon) + Creature* nearestCorpse = nullptr; + float nearestDist = std::numeric_limits::max(); + static const uint32 corpseEntries[] = { + NPC_DEAD_SCARLET_MEDIC, NPC_DEAD_SCARLET_INFANTRYMAN, NPC_DEAD_SCARLET_CAPTAIN, + NPC_DEAD_SCARLET_PEASANT, NPC_DEAD_SCARLET_MINER, NPC_DEAD_SCARLET_FLEET_DEFENDER + }; + for (uint32 entry : corpseEntries) + { + // Search up to 60 yards for each type + if (Creature* corpse = me->FindNearestCreature(entry, 60.0f, true)) + { + float dist = me->GetDistance(corpse); + if (dist < nearestDist) + { + nearestDist = dist; + nearestCorpse = corpse; + } + } + } + if (!nearestCorpse) + { + // No corpse found nearby: try again later + events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); + break; + } + // Start ritual + isOnRitual = true; + targetCorpseGUID = nearestCorpse->GetGUID(); + geistGUID.Clear(); + // Pause waypoint movement and move to the corpse + me->PauseMovement(); + float x, y, z; + // Keep it at a distance from the corpse + nearestCorpse->GetClosePoint(x, y, z, me->GetObjectSize()); + me->GetMotionMaster()->MovePoint(POINT_CORPSE_REACHED, x, y, z); + break; + } + + case EVENT_GHOULPLOSION: + { + if (Creature* geist = ObjectAccessor::GetCreature(*me, geistGUID)) + { + me->SetFacingToObject(geist); + DoCast(geist, SPELL_GHOULPLOSION); + } + break; + } + + case EVENT_RAISE_GHOUL: + { + if (Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID)) + { + // Cast Scarlet Ghoul on the corpse (always a humanoid for necromancer) + me->SetFacingToObject(corpse); + DoCast(corpse, SPELL_SCARLET_GHOUL); + } + break; + } + + case EVENT_RESUME_WP: + { + // Resume waypoint movement + isOnRitual = false; + + targetCorpseGUID.Clear(); + + // Resume paused waypoint movement + me->ResumeMovement(); + // Schedule next ritual in 20-30s + events.ScheduleEvent(EVENT_START_RITUAL, 20s, 30s); + break; + } + } + } + + // Necromancers are not expected to engage in combat; no melee UpdateAI needed beyond events. + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type == POINT_MOTION_TYPE && id == POINT_CORPSE_REACHED) + { + // Reached the corpse + // Check for nearby Gluttonous Geist within ~3 yards + Creature* geist = me->FindNearestCreature(NPC_GLUTTONOUS_GEIST, 3.0f, true); + if (geist) + { + me->SetFacingToObject(geist); + geistGUID = geist->GetGUID(); + // Geist found: schedule Ghoulplosion at +3s, then raising at +6s, then resume at +9s + events.ScheduleEvent(EVENT_GHOULPLOSION, 3s); + events.ScheduleEvent(EVENT_RAISE_GHOUL, 6s); + events.ScheduleEvent(EVENT_RESUME_WP, 9s); + } + else + { + // No Geist: just raise after 3s, resume 3s later + + Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID); + if (corpse) + { + me->SetFacingToObject(corpse); + } + + events.ScheduleEvent(EVENT_RAISE_GHOUL, 3s); + events.ScheduleEvent(EVENT_RESUME_WP, 6s); + } + } + } +}; + +struct npc_gothik_the_harvester : public ScriptedAI +{ + npc_gothik_the_harvester(Creature* creature) : ScriptedAI(creature) { } + + EventMap events; + ObjectGuid targetCorpseGUID; + ObjectGuid geistGUID; + bool isOnRitual; + + enum Events + { + EVENT_START_RITUAL = 1, + EVENT_GHOULPLOSION, + EVENT_RAISE_DEAD, + EVENT_RESUME_WP + }; + + enum Points + { + POINT_CORPSE_REACHED = 1 + }; + + // Text identifiers for creature_text (see SQL below) + enum Says + { + SAY_GRYPHON = 0, // "You will fly again, beast..." + SAY_GHOUL = 1, // "Surprise, surprise! Another ghoul!" + SAY_GEIST = 2 // "Is Gothik the Harvester going to have to choke a geist?" + }; + + void Reset() override + { + events.Reset(); + targetCorpseGUID.Clear(); + geistGUID.Clear(); + isOnRitual = false; + // Start waypoint movement using WaypointMovementGenerator + if (uint32 pathId = me->GetWaypointPath()) + { + me->GetMotionMaster()->MoveWaypoint(pathId, true); // true = repeatable + } + // Schedule the first ritual after 50-60s + events.ScheduleEvent(EVENT_START_RITUAL, 50s, 60s); + } + void UpdateAI(uint32 diff) override + { + events.Update(diff); + + if (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_RITUAL: + { + if (isOnRitual) // Already performing ritual + { + events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); + break; + } + + // Find nearest dead Scarlet NPC (including gryphon) + Creature* nearestCorpse = nullptr; + float nearestDist = std::numeric_limits::max(); + static const uint32 corpseEntries[] = { + NPC_DEAD_SCARLET_MEDIC, NPC_DEAD_SCARLET_INFANTRYMAN, NPC_DEAD_SCARLET_CAPTAIN, + NPC_DEAD_SCARLET_PEASANT, NPC_DEAD_SCARLET_MINER, NPC_DEAD_SCARLET_FLEET_DEFENDER, + NPC_DEAD_SCARLET_GRYPHON + }; + for (uint32 entry : corpseEntries) + { + // Search up to 60 yards for each type + if (Creature* corpse = me->FindNearestCreature(entry, 60.0f, true)) + { + float dist = me->GetDistance(corpse); + if (dist < nearestDist) + { + nearestDist = dist; + nearestCorpse = corpse; + } + } + } + if (!nearestCorpse) + { + events.ScheduleEvent(EVENT_START_RITUAL, 5s, 10s); + break; + } + // Start ritual + isOnRitual = true; + targetCorpseGUID = nearestCorpse->GetGUID(); + geistGUID.Clear(); + // Pause waypoint movement and move to the corpse + me->PauseMovement(); + float x, y, z; + // Keep it at a distance from the corpse + nearestCorpse->GetClosePoint(x, y, z, me->GetObjectSize()); + me->GetMotionMaster()->MovePoint(POINT_CORPSE_REACHED, x, y, z); + break; + } + case EVENT_GHOULPLOSION: + { + // Cast Ghoulplosion on the Geist and say the Geist line + if (Creature* geist = ObjectAccessor::GetCreature(*me, geistGUID)) + { + Talk(SAY_GEIST); + me->SetFacingToObject(geist); + DoCast(geist, SPELL_GHOULPLOSION); + } + break; + } + + case EVENT_RAISE_DEAD: + { + // Cast the appropriate raise spell on the corpse (griffon or ghoul) + if (Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID)) + { + me->SetFacingToObject(corpse); + uint32 entry = corpse->GetEntry(); + if (entry == NPC_DEAD_SCARLET_GRYPHON) + { + DoCast(corpse, SPELL_SCOURGE_GRYPHON); + } + else + { + DoCast(corpse, SPELL_SCARLET_GHOUL); + } + } + break; + } + case EVENT_RESUME_WP: + { + // Resume waypoint movement + isOnRitual = false; + targetCorpseGUID.Clear(); + // Resume paused waypoint movement + me->ResumeMovement(); + // Schedule next ritual in 50-60s + events.ScheduleEvent(EVENT_START_RITUAL, 50s, 60s); + break; + } + } + } + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type == POINT_MOTION_TYPE && id == POINT_CORPSE_REACHED) + { + // Reached the target corpse + Creature* corpse = ObjectAccessor::GetCreature(*me, targetCorpseGUID); + if (corpse) + { + me->SetFacingToObject(corpse); + // Say line depending on corpse type (gryphon or humanoid) + if (corpse->GetEntry() == NPC_DEAD_SCARLET_GRYPHON) + Talk(SAY_GRYPHON); + else + Talk(SAY_GHOUL); + } + // Check for Geist nearby + Creature* geist = me->FindNearestCreature(NPC_GLUTTONOUS_GEIST, 3.0f, true); + if (geist) + { + me->SetFacingToObject(geist); + geistGUID = geist->GetGUID(); + // Geist present: Ghoulplosion in 3s (with SAY_GEIST), raise in 6s, resume in 9s + events.ScheduleEvent(EVENT_GHOULPLOSION, 3s); + events.ScheduleEvent(EVENT_RAISE_DEAD, 6s); + events.ScheduleEvent(EVENT_RESUME_WP, 9s); + } + else + { + // No Geist: raise in 3s, resume in 6s + events.ScheduleEvent(EVENT_RAISE_DEAD, 3s); + events.ScheduleEvent(EVENT_RESUME_WP, 6s); + } + } + } +}; + +/*###### +## Persuasive Strike (Spell Script) +## Quest: How To Win Friends And Influence Enemies +## Note: texts signed for creature 28939 but used for 28939, 28940, 28610 +######*/ + +enum win_friends +{ + SAY_CRUSADER = 1, + SAY_PERSUADED1 = 2, + SAY_PERSUADED2 = 3, + SAY_PERSUADED3 = 4, + SAY_PERSUADED4 = 5, + SAY_PERSUADED5 = 6, + SAY_PERSUADED6 = 7, + SAY_PERSUADE_RAND = 8, + QUEST_HOW_TO_WIN_FRIENDS = 12720, + + NPC_SCARLET_PREACHER = 28939, + NPC_SCARLET_COMMANDER = 28936, + NPC_SCARLET_CRUSADER = 28940, + NPC_SCARLET_MARKSMAN = 28610, + NPC_SCARLET_LORD_MCCREE = 28964 +}; + +// 52781 - Persuasive Strike +class spell_chapter2_persuasive_strike : public SpellScript +{ + PrepareSpellScript(spell_chapter2_persuasive_strike); + + bool Load() override + { + return GetCaster() && GetCaster()->IsPlayer() + && GetCaster()->ToPlayer()->GetQuestStatus(QUEST_HOW_TO_WIN_FRIENDS) == QUEST_STATUS_INCOMPLETE; + } + + void HandleHit(SpellEffIndex /*effIndex*/) + { + Creature* creature = GetHitCreature(); + Player* player = GetCaster()->ToPlayer(); + + if (!creature || !player) + return; + + if (!creature->EntryEquals(NPC_SCARLET_PREACHER, NPC_SCARLET_COMMANDER, NPC_SCARLET_CRUSADER, NPC_SCARLET_MARKSMAN, NPC_SCARLET_LORD_MCCREE)) + return; + + sCreatureTextMgr->SendChat(creature, SAY_PERSUADE_RAND, nullptr, CHAT_MSG_ADDON, LANG_ADDON, TEXT_RANGE_NORMAL, 0, TEAM_NEUTRAL, false, player); + + if (roll_chance_f(30.0f)) + { + creature->CombatStop(true); + creature->GetMotionMaster()->MoveIdle(); + creature->SetImmuneToPC(true); + creature->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + creature->SetReactState(REACT_PASSIVE); + + creature->AI()->Talk(SAY_PERSUADED1, 8s); + creature->AI()->Talk(SAY_PERSUADED2, 16s); + creature->AI()->Talk(SAY_PERSUADED3, 24s); + creature->AI()->Talk(SAY_PERSUADED4, 32s); + + ObjectGuid playerGuid = player->GetGUID(); + + creature->m_Events.AddEventAtOffset([creature, playerGuid] + { + if (Player* caster = ObjectAccessor::GetPlayer(*creature, playerGuid)) + sCreatureTextMgr->SendChat(creature, SAY_PERSUADED5, nullptr, CHAT_MSG_ADDON, LANG_ADDON, TEXT_RANGE_NORMAL, 0, TEAM_NEUTRAL, false, caster); + }, 40s); + + creature->m_Events.AddEventAtOffset([creature, playerGuid] + { + creature->AI()->Talk(SAY_PERSUADED6); + + if (Player* caster = ObjectAccessor::GetPlayer(*creature, playerGuid)) + { + Unit::Kill(caster, creature); + caster->GroupEventHappens(QUEST_HOW_TO_WIN_FRIENDS, creature); + } + else + creature->KillSelf(); + }, 48s); + } + else + creature->AI()->Talk(SAY_CRUSADER, 1s); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_chapter2_persuasive_strike::HandleHit, EFFECT_0, SPELL_EFFECT_DUMMY); + } +}; + +/*###### +## Portal Effect: Acherus (Spell Script) +## Quest: Scarlet Armies Approach... +## Note: Casted by Orbaz Bloodbane +######*/ + +enum AcherusPortal +{ + SPELL_PORTAL_EFFECT_ACHERUS = 53098, + QUEST_SCARLET_ARMIES_APPROACH = 12757 +}; + +class spell_portal_effect_acherus : public SpellScript +{ + PrepareSpellScript(spell_portal_effect_acherus); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_PORTAL_EFFECT_ACHERUS }); + } + + SpellCastResult CheckCast() + { + Unit* target = GetExplTargetUnit(); + if (target && target->IsPlayer() && target->ToPlayer()->HasQuest(QUEST_SCARLET_ARMIES_APPROACH)) + return SPELL_CAST_OK; + + return SPELL_FAILED_DONT_REPORT; + } + + void HandleScriptEffect(SpellEffIndex /*effIndex*/) + { + if (Unit* caster = GetCaster()) + if (Player* player = GetHitPlayer()) + caster->CastSpell(player, GetEffectValue(), true); + } + + void Register() override + { + OnCheckCast += SpellCheckCastFn(spell_portal_effect_acherus::CheckCast); + OnEffectHitTarget += SpellEffectFn(spell_portal_effect_acherus::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +/*###### +## Devour Humanoid (Spell Script) +## Quest: An End To All Things... +## Note: Used by Frostbrood Vanquisher +######*/ + +enum DevourHumanoid +{ + NPC_HEARTHGLEN_CRUSADER = 29102, + NPC_TIRISFAL_CRUSADER = 29103 +}; + +// 53110 - Devour Humanoid +class spell_q12779_an_end_to_all_things : public SpellScript +{ + PrepareSpellScript(spell_q12779_an_end_to_all_things); + + SpellCastResult CheckCast() + { + if (Unit* caster = GetCaster()) + if (caster->FindNearestCreature(NPC_HEARTHGLEN_CRUSADER, 15.0f, true) || caster->FindNearestCreature(NPC_TIRISFAL_CRUSADER, 15.0f, true)) + return SPELL_CAST_OK; + + return SPELL_FAILED_BAD_TARGETS; + } + + void HandleScriptEffect(SpellEffIndex /*effIndex*/) + { + if (Creature* c = GetHitUnit()->ToCreature()) + if (Unit* caster = GetCaster()) + { + c->AI()->AttackStart(caster); + c->CastSpell(caster, GetEffectValue(), true); // 53111 + } + } + + void Register() override + { + OnCheckCast += SpellCheckCastFn(spell_q12779_an_end_to_all_things::CheckCast); + OnEffectHitTarget += SpellEffectFn(spell_q12779_an_end_to_all_things::HandleScriptEffect, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 53111 - Devour Humanoid (casted by the devoured creature) +class spell_q12779_an_end_to_all_things_devour_aura : public AuraScript +{ + PrepareAuraScript(spell_q12779_an_end_to_all_things_devour_aura); + + void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) + { + Unit* caster = GetCaster(); + Unit* target = GetTarget(); + if (!caster || !target) + return; + + if (GetTargetApplication()->GetRemoveMode() == AURA_REMOVE_BY_EXPIRE) + { + caster->SetDisableGravity(true); + Unit::Kill(target, caster); + } + } + + void Register() override + { + AfterEffectRemove += AuraEffectRemoveFn(spell_q12779_an_end_to_all_things_devour_aura::OnRemove, EFFECT_0, SPELL_AURA_CONTROL_VEHICLE, AURA_EFFECT_HANDLE_REAL); + } +}; + +/*###### +## The Light of Dawn Game Event +## Quest: The Light of Dawn +######*/ + +enum LightOfDawnSays +{ + SAY_LIGHT_OF_DAWN01 = 0, // pre text + SAY_LIGHT_OF_DAWN02 = 1, + SAY_LIGHT_OF_DAWN03 = 2, + SAY_LIGHT_OF_DAWN04 = 3, // intro + SAY_LIGHT_OF_DAWN05 = 4, + SAY_LIGHT_OF_DAWN06 = 5, + SAY_LIGHT_OF_DAWN07 = 6, // During the fight - Korfax, Champion of the Light + SAY_LIGHT_OF_DAWN08 = 7, // Lord Maxwell Tyrosus + SAY_LIGHT_OF_DAWN09 = 8, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN25 = 24, // After the fight + SAY_LIGHT_OF_DAWN26 = 25, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN27 = 26, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN28 = 27, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN29 = 28, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN30 = 29, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN31 = 30, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN32 = 31, // Highlord Alexandros Mograine + SAY_LIGHT_OF_DAWN33 = 32, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN34 = 33, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN35 = 34, // Darion Mograine + SAY_LIGHT_OF_DAWN36 = 35, // Darion Mograine + SAY_LIGHT_OF_DAWN37 = 36, // Highlord Alexandros Mograine + SAY_LIGHT_OF_DAWN38 = 37, // Darion Mograine + SAY_LIGHT_OF_DAWN39 = 38, // Highlord Alexandros Mograine + SAY_LIGHT_OF_DAWN40 = 39, // Darion Mograine + SAY_LIGHT_OF_DAWN41 = 40, // Highlord Alexandros Mograine + SAY_LIGHT_OF_DAWN42 = 41, // Highlord Alexandros Mograine + SAY_LIGHT_OF_DAWN43 = 42, // The Lich King + SAY_LIGHT_OF_DAWN44 = 43, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN45 = 44, // The Lich King + SAY_LIGHT_OF_DAWN46 = 45, // The Lich King + SAY_LIGHT_OF_DAWN47 = 46, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN48 = 47, // The Lich King + SAY_LIGHT_OF_DAWN49 = 48, // The Lich King + SAY_LIGHT_OF_DAWN50 = 49, // Lord Maxwell Tyrosus + SAY_LIGHT_OF_DAWN51 = 50, // The Lich King + SAY_LIGHT_OF_DAWN52 = 51, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN53 = 52, // Highlord Darion Mograine + SAY_LIGHT_OF_DAWN54 = 53, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN55 = 54, // The Lich King + SAY_LIGHT_OF_DAWN56 = 55, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN57 = 56, // The Lich King + SAY_LIGHT_OF_DAWN58 = 57, // The Lich King + SAY_LIGHT_OF_DAWN59 = 58, // The Lich King + SAY_LIGHT_OF_DAWN60 = 59, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN61 = 60, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN62 = 61, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN63 = 62, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN64 = 63, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN65 = 64, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN66 = 65, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN67 = 66, // Highlord Tirion Fordring + SAY_LIGHT_OF_DAWN68 = 67, // Highlord Darion Mograine + + EMOTE_LIGHT_OF_DAWN01 = 68, // Emotes + EMOTE_LIGHT_OF_DAWN02 = 69, + EMOTE_LIGHT_OF_DAWN03 = 70, + EMOTE_LIGHT_OF_DAWN04 = 71, + EMOTE_LIGHT_OF_DAWN05 = 72, + EMOTE_LIGHT_OF_DAWN06 = 73, + EMOTE_LIGHT_OF_DAWN07 = 74, + EMOTE_LIGHT_OF_DAWN08 = 75, + EMOTE_LIGHT_OF_DAWN09 = 76, + EMOTE_LIGHT_OF_DAWN10 = 77, + EMOTE_LIGHT_OF_DAWN11 = 78, + EMOTE_LIGHT_OF_DAWN12 = 79, + EMOTE_LIGHT_OF_DAWN13 = 80, + EMOTE_LIGHT_OF_DAWN14 = 81, + EMOTE_LIGHT_OF_DAWN15 = 82, + EMOTE_LIGHT_OF_DAWN16 = 83, + EMOTE_LIGHT_OF_DAWN17 = 84, + EMOTE_LIGHT_OF_DAWN18 = 85 +}; + +enum LightOfDawnEncounter +{ + // Intro Events + EVENT_START_COUNTDOWN_1 = 1, + EVENT_START_COUNTDOWN_2, + EVENT_START_COUNTDOWN_3, + EVENT_START_COUNTDOWN_4, + EVENT_START_COUNTDOWN_5, + EVENT_START_COUNTDOWN_6, + EVENT_START_COUNTDOWN_7, + EVENT_START_COUNTDOWN_8, + EVENT_START_COUNTDOWN_9, + EVENT_START_COUNTDOWN_10, + EVENT_START_COUNTDOWN_11, + EVENT_START_COUNTDOWN_12, + EVENT_START_COUNTDOWN_13, + EVENT_START_COUNTDOWN_14, + // Fight Events + EVENT_SPELL_ANTI_MAGIC_ZONE, + EVENT_SPELL_DEATH_STRIKE, + EVENT_SPELL_DEATH_EMBRACE, + EVENT_SPELL_UNHOLY_BLIGHT, + EVENT_SPELL_DARION_MOD_DAMAGE, + // Positioning + EVENT_FINISH_FIGHT_1, + EVENT_FINISH_FIGHT_2, + EVENT_FINISH_FIGHT_3, + EVENT_FINISH_FIGHT_4, + EVENT_FINISH_FIGHT_5, + // Outro + EVENT_OUTRO_SCENE_1, + EVENT_OUTRO_SCENE_2, + EVENT_OUTRO_SCENE_3, + EVENT_OUTRO_SCENE_4, + EVENT_OUTRO_SCENE_5, + EVENT_OUTRO_SCENE_6, + EVENT_OUTRO_SCENE_7, + EVENT_OUTRO_SCENE_8, + EVENT_OUTRO_SCENE_9, + EVENT_OUTRO_SCENE_10, + EVENT_OUTRO_SCENE_11, + EVENT_OUTRO_SCENE_12, + EVENT_OUTRO_SCENE_13, + EVENT_OUTRO_SCENE_14, + EVENT_OUTRO_SCENE_15, + EVENT_OUTRO_SCENE_16, + EVENT_OUTRO_SCENE_17, + EVENT_OUTRO_SCENE_18, + EVENT_OUTRO_SCENE_19, + EVENT_OUTRO_SCENE_20, + EVENT_OUTRO_SCENE_21, + EVENT_OUTRO_SCENE_22, + EVENT_OUTRO_SCENE_23, + EVENT_OUTRO_SCENE_24, + EVENT_OUTRO_SCENE_25, + EVENT_OUTRO_SCENE_26, + EVENT_OUTRO_SCENE_27, + EVENT_OUTRO_SCENE_28, + EVENT_OUTRO_SCENE_29, + EVENT_OUTRO_SCENE_30, + EVENT_OUTRO_SCENE_31, + EVENT_OUTRO_SCENE_32, + EVENT_OUTRO_SCENE_33, + EVENT_OUTRO_SCENE_34, + EVENT_OUTRO_SCENE_35, + EVENT_OUTRO_SCENE_36, + EVENT_OUTRO_SCENE_37, + EVENT_OUTRO_SCENE_38, + EVENT_OUTRO_SCENE_39, + EVENT_OUTRO_SCENE_40, + EVENT_OUTRO_SCENE_41, + EVENT_OUTRO_SCENE_42, + EVENT_OUTRO_SCENE_43, + EVENT_OUTRO_SCENE_44, + EVENT_OUTRO_SCENE_45, + EVENT_OUTRO_SCENE_46, + EVENT_OUTRO_SCENE_47, + EVENT_OUTRO_SCENE_48, + EVENT_OUTRO_SCENE_49, + EVENT_OUTRO_SCENE_50, + EVENT_OUTRO_SCENE_51, + EVENT_OUTRO_SCENE_52, + EVENT_OUTRO_SCENE_53, + EVENT_OUTRO_SCENE_54, + EVENT_OUTRO_SCENE_55, + EVENT_OUTRO_SCENE_56, + EVENT_OUTRO_SCENE_57, + EVENT_OUTRO_SCENE_58, + EVENT_OUTRO_SCENE_59, + EVENT_OUTRO_SCENE_60, + EVENT_OUTRO_SCENE_61, + + ACTION_START_EVENT = 1, + ACTION_PLAY_EMOTE = 1, + ACTION_POSITION_NPCS = 2, + + ENCOUNTER_START_TIME = 5, + ENCOUNTER_TOTAL_DEFENDERS = 300, + ENCOUNTER_TOTAL_SCOURGE = 10000, + + ENCOUNTER_STATE_NONE = 0, + ENCOUNTER_STATE_FIGHT = 1, + ENCOUNTER_STATE_OUTRO = 2, +}; + +enum LightOfDawnNPCs +{ + // Defenders + NPC_DEFENDER_OF_THE_LIGHT = 29174, + NPC_KORFAX_CHAMPION_OF_THE_LIGHT = 29176, + NPC_COMMANDER_ELIGOR_DAWNBRINGER = 29177, + NPC_LORD_MAXWELL_TYROSUS = 29178, + NPC_LEONID_BARTHALOMEW_THE_REVERED = 29179, + NPC_DUKE_NICHOLAS_ZVERENHOFF = 29180, + NPC_RAYNE = 29181, + NPC_RIMBLAT_EARTHSHATTER = 29182, + + // Scourge + NPC_RAMPAGING_ABOMINATION = 29186, + NPC_ACHERUS_GHOUL = 29219, + NPC_WARRIOR_OF_THE_FROZEN_WASTES = 29206, + NPC_FLESH_BEHEMOTH = 29190, + + NPC_HIGHLORD_DARION_MOGRAINE = 29173, + NPC_KOLTIRA_DEATHWEAVER = 29199, + NPC_ORBAZ_BLOODBANE = 29204, + NPC_THASSARIAN = 29200, + + // Outro + NPC_HIGHLORD_TIRION_FORDRING = 29175, + NPC_HIGHLORD_ALEXANDROS_MOGRAINE = 29227, // ghost + NPC_DARION_MOGRAINE = 29228, // ghost + NPC_THE_LICH_KING = 29183, +}; + +enum LightOfDawnGOs +{ + GO_HOLY_LIGHTNING = 191301, + GO_LIGHT_OF_DAWN = 191330 +}; + +enum LightOfDawnSpells +{ + // Intro Spells + SPELL_CAMERA_SHAKE_INIT = 36455, + SPELL_CAMERA_SHAKE = 39983, + SPELL_THE_MIGHT_OF_MOGRAINE = 53642, + + // Mograine Fight + SPELL_ANTI_MAGIC_ZONE1 = 52893, + SPELL_DEATH_STRIKE = 53639, + SPELL_DEATH_EMBRACE = 53635, + SPELL_ICY_TOUCH1 = 49723, + SPELL_UNHOLY_BLIGHT = 53640, + SPELL_DARION_MOD_DAMAGE = 53645, + + // Outro + SPELL_THE_LIGHT_OF_DAWN = 53658, + SPELL_ALEXANDROS_MOGRAINE_SPAWN = 53667, + SPELL_ICEBOUND_VISAGE = 53274, + SPELL_SOUL_FEAST_ALEX = 53677, + SPELL_MOGRAINE_CHARGE = 53679, + SPELL_REBUKE = 53680, + SPELL_SOUL_FEAST_TIRION = 53685, + SPELL_APOCALYPSE = 53210, + SPELL_THROW_ASHBRINGER = 53701, + SPELL_REBIRTH_OF_THE_ASHBRINGER = 53702, + SPELL_TIRION_CHARGE = 53705, + SPELL_EXIT_TELEPORT_VISUAL = 61456, + SPELL_LAY_ON_HANDS = 53778, + SPELL_THE_LIGHT_OF_DAWN_Q = 53606 +}; + +const Position LightOfDawnPos[] = +{ + {2304.2f, -5290.7f, 82.01f, 4.56f}, // 0 First Home Pos + {2253.5f, -5310.6f, 82.17f, 5.28f}, // 1 Second Home Pos + {2169.1f, -5227.1f, 82.59f, 5.7f}, // 2 Orbaz Flee Pos + {2289.259f, -5280.355f, 86.112f, 4.41f}, // 3 Koltira Loc1 + {2273.289f, -5273.675f, 86.701f, 5.01f}, // 4 Thassarian Loc1 + {2280.81f, -5284.09f, 86.608f, 4.76f}, // 5 Morgraine Loc1 + {2165.711f, -5266.1235f, 95.5025f, 0.13962634f }, // 6 Tirion Summon loc + {2281.198f, -5257.397f, 80.224f, 4.66f}, // 7 Alexandros loc1 + {2281.156f, -5259.934f, 80.647f, 0}, // 8 Alexandros loc2 + {2281.294f, -5281.895f, 82.445f, 1.35f}, // 9 Darion loc1 + {2281.093f, -5263.013f, 81.125f, 0}, // 10 Darion loc2 + {2283.896f, -5287.914f, 83.066f, 1.55f}, // 11 Tirion Fordring loc2 + {2280.304f, -5257.205f, 80.09781f, 4.6251f },// 12 Lich King spawns + {2280.687f, -5262.276f, 81.082634f, 0.0f },// 13 Lich king moves forward + {2264.27f, -5267.29f, 80.16f, 0}, // 14 Tirion Fordring loc3 + {2270.99f, -5278.00f, 81.89f, 0} // 15 Tirion Fordring loc4 +}; + +const Position LightOfDawnFightPos[] = +{ + {2279.68f, -5256.75f, 79.79f, 4.8f}, + {2280.40f, -5276.56f, 82.11f, 4.8f}, + {2256.43f, -5281.3f, 82.29f, 5.0f}, + {2251.87f, -5304.08f, 82.17f, 4.8f}, + {2244.88f, -5256.03f, 74.88f, 5.8f}, + {2294.29f, -5281.35f, 81.91f, 4.8f}, + {2314.2f, -5268.1f, 82.43f, 3.6f}, + {2289.72f, -5299.65f, 83.49f, 3.2f}, + {2274.02f, -5303.58f, 85.05f, 1.4f}, + {2258.42f, -5307.72f, 81.98f, 0.1f} +}; + +class DelayedSummonEvent : public BasicEvent +{ +public: + DelayedSummonEvent(Unit* owner, uint32 entry, Position pos) : _owner(owner), _entry(entry), _pos(pos) { } + + bool Execute(uint64 /*eventTime*/, uint32 /*updateTime*/) override + { + _owner->SummonCreature(_entry, _pos, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 3000); + return true; + } + +private: + Unit* _owner; + uint32 _entry; + Position _pos; +}; + +struct npc_highlord_darion_mograine : public ScriptedAI +{ + npc_highlord_darion_mograine(Creature* creature) : ScriptedAI(creature), summons(me) + { + battleStarted = ENCOUNTER_STATE_NONE; + me->SetCorpseDelay(3 * 60); + me->SetRespawnTime(3 * 60); + resetExecuted = false; + } + + EventMap events; + SummonList summons; + uint32 startTimeRemaining; + uint32 defendersRemaining; + uint32 scourgeRemaining; + uint8 battleStarted; + bool resetExecuted; + + void sGossipHello(Player* player) override + { + ClearGossipMenuFor(player); + + if (me->IsQuestGiver()) + player->PrepareQuestMenu(me->GetGUID()); + + if (player->GetQuestStatus(12801) == QUEST_STATUS_INCOMPLETE && !GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE)) + AddGossipItemFor(player, 9795, 0, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); + + SendGossipMenuFor(player, player->GetGossipTextId(me), me->GetGUID()); + } + + void sGossipSelect(Player* player, uint32 /*menuId*/, uint32 gossipListId) override + { + if (player->PlayerTalkClass->GetGossipOptionAction(gossipListId) == GOSSIP_ACTION_INFO_DEF + 1) + { + ClearGossipMenuFor(player); + CloseGossipMenuFor(player); + DoAction(ACTION_START_EVENT); + } + } + + void DoAction(int32 param) override + { + if (param == ACTION_START_EVENT && !startTimeRemaining && events.Empty()) + { + Talk(SAY_LIGHT_OF_DAWN01); + + startTimeRemaining = ENCOUNTER_START_TIME; + defendersRemaining = ENCOUNTER_TOTAL_DEFENDERS; + scourgeRemaining = ENCOUNTER_TOTAL_SCOURGE; + + SendInitialWorldStates(); + + events.Reset(); + events.ScheduleEvent(EVENT_START_COUNTDOWN_1, 60s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_2, 120s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_3, 180s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_4, 240s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_5, 300s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_6, 308s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_7, 312s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_8, 316s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_9, 320s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_10, 324s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_11, 332s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_12, 335s); + events.ScheduleEvent(EVENT_START_COUNTDOWN_13, 337s + 500ms); + events.ScheduleEvent(EVENT_START_COUNTDOWN_14, 345s); + } + } + + uint32 GetData(uint32 type) const override + { + switch (type) + { + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT: + return defendersRemaining; + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT: + return scourgeRemaining; + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE: + return me->IsAlive() && (startTimeRemaining || battleStarted); + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE: + return me->IsAlive() && startTimeRemaining; + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME: + return startTimeRemaining; + case WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE: + return me->IsAlive() && !startTimeRemaining && battleStarted; + } + return 0; + } + + void SendUpdateWorldState(uint32 id, uint32 state) + { + Map::PlayerList const& players = me->GetMap()->GetPlayers(); + if (!players.IsEmpty()) + for (Map::PlayerList::const_iterator itr = players.begin(); itr != players.end(); ++itr) + if (Player* player = itr->GetSource()) + if (player->GetPhaseMask() & 128) // Xinef: client skips players without chapter 5 aura anyway, speedup + player->SendUpdateWorldState(id, state); + } + + void SendInitialWorldStates() + { + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT)); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT)); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SOLDIERS_ENABLE)); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE)); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME)); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE)); + } + + void JustSummoned(Creature* cr) override + { + summons.Summon(cr); + + if (me->IsInCombat() && cr->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING && battleStarted == ENCOUNTER_STATE_FIGHT) + { + Position pos = LightOfDawnFightPos[urand(0, 9)]; + if (Unit* target = cr->SelectNearbyTarget(nullptr, 10.0f)) + if (target->IsCreature()) + target->GetMotionMaster()->MoveCharge(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), me->GetSpeed(MOVE_RUN)); + cr->GetMotionMaster()->MoveCharge(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), me->GetSpeed(MOVE_RUN)); + } + + if (battleStarted == ENCOUNTER_STATE_OUTRO && cr->GetEntry() == NPC_DEFENDER_OF_THE_LIGHT) + { + cr->SetReactState(REACT_PASSIVE); + cr->SetImmuneToAll(true); + cr->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_READY1H); + cr->HandleEmoteCommand(EMOTE_STATE_READY1H); + } + } + + void SummonedCreatureDies(Creature* creature, Unit*) override + { + // Refill Armies and update counters + if (battleStarted != ENCOUNTER_STATE_FIGHT) + return; + + me->m_Events.AddEventAtOffset(new DelayedSummonEvent(me, creature->GetEntry(), *creature), 3s); + if (creature->GetEntry() >= NPC_RAMPAGING_ABOMINATION) + { + --scourgeRemaining; + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_SCOURGE_COUNT)); + } + else + { + --defendersRemaining; + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT, GetData(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_DEFENDERS_COUNT)); + + if (defendersRemaining == 200) + FinishFight(); + } + } + + void JustDied(Unit*) override + { + summons.DespawnAll(); + me->SetCorpseDelay(3 * 60); + me->SetRespawnTime(3 * 60); + } + + void FinishFight() + { + if (Creature* tirion = me->SummonCreature(NPC_HIGHLORD_TIRION_FORDRING, LightOfDawnPos[6], TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 600000)) + { + tirion->LoadEquipment(0, true); + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN25, 4s); + + tirion->m_Events.AddEventAtOffset([&, tirion] { + tirion->GetMotionMaster()->MoveWaypoint(NPC_HIGHLORD_TIRION_FORDRING * 10, false); + }, 14s); + + events.Reset(); + events.ScheduleEvent(EVENT_FINISH_FIGHT_1, 10s); + events.ScheduleEvent(EVENT_FINISH_FIGHT_2, 20s); + events.ScheduleEvent(EVENT_FINISH_FIGHT_3, 22s); + events.ScheduleEvent(EVENT_FINISH_FIGHT_4, 23s); + events.ScheduleEvent(EVENT_FINISH_FIGHT_5, 24s); + + tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2254.84f, -5298.75f, 82.168f, 1.134f, 0, 0, 0.537102f, 0.843517f, 20); + tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2296.24f, -5296.44f, 81.9964f, 5.3398f, 0, 0, 0.454395f, -0.8908f, 20); + tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2314.29f, -5261.78f, 83.1349f, 3.05822f, 0, 0, 0.999131f, 0.0416735f, 20); + tirion->SummonGameObject(GO_HOLY_LIGHTNING, 2278.43f, -5270.14f, 81.7247f, 0.70988f, 0, 0, 0.347534f, 0.937667f, 20); + } + } + + void JustEngagedWith(Unit*) override + { + if (battleStarted != ENCOUNTER_STATE_FIGHT) + return; + + events.RescheduleEvent(EVENT_SPELL_ANTI_MAGIC_ZONE, 15s); + events.RescheduleEvent(EVENT_SPELL_DEATH_STRIKE, 8s); + events.RescheduleEvent(EVENT_SPELL_DEATH_EMBRACE, 5s); + events.RescheduleEvent(EVENT_SPELL_UNHOLY_BLIGHT, 10s); + events.RescheduleEvent(EVENT_SPELL_DARION_MOD_DAMAGE, 500ms); + } + + void Reset() override + { + if (resetExecuted) + return; + + resetExecuted = true; + JustRespawned(); + } + + void JustRespawned() override + { + events.Reset(); + summons.DespawnAll(); + + me->SetImmuneToAll(true); + me->LoadEquipment(1, true); + me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_GOSSIP | UNIT_NPC_FLAG_QUESTGIVER); + me->SetStandState(UNIT_STAND_STATE_STAND); + me->SetVisible(true); + me->setActive(true); + me->SetWalk(false); + + battleStarted = ENCOUNTER_STATE_NONE; + startTimeRemaining = 0; + defendersRemaining = 0; + scourgeRemaining = 0; + + SendInitialWorldStates(); + me->SummonCreatureGroup(30); + } + + Creature* GetEntryFromSummons(uint32 entry) + { + for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) + if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) + if (summon->GetEntry() == entry) + return summon; + return nullptr; + } + + void MovementInform(uint32 type, uint32 point) override + { + if (type == POINT_MOTION_TYPE && point == 2) + { + me->RemoveAurasDueToSpell(SPELL_THE_LIGHT_OF_DAWN); + Talk(EMOTE_LIGHT_OF_DAWN05); + events.Reset(); + + events.ScheduleEvent(EVENT_OUTRO_SCENE_1, 2s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_2, 19s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_3, 38s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_4, 50s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_5, 62s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_6, 68s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_7, 71s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_8, 72s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_9, 74s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_10, 77s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_11, 79s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_12, 82s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_13, 85s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_14, 92s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_15, 98s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_16, 105s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_17, 120s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_18, 131s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_19, 158s); + } + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + uint32 eventId = events.ExecuteEvent(); + + switch (eventId) + { + case EVENT_START_COUNTDOWN_1: + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 4); + break; + case EVENT_START_COUNTDOWN_2: + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 3); + break; + case EVENT_START_COUNTDOWN_3: + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 2); + break; + case EVENT_START_COUNTDOWN_4: + Talk(SAY_LIGHT_OF_DAWN02); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 1); + break; + case EVENT_START_COUNTDOWN_5: + battleStarted = ENCOUNTER_STATE_FIGHT; + me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_NONE); + Talk(SAY_LIGHT_OF_DAWN04); // Wrong order in DB! + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_TIME, 0); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_COUNTDOWN_ENABLE, 0); + SendUpdateWorldState(WORLD_STATE_BATTLE_FOR_LIGHTS_HOPE_EVENT_BEGIN_ENABLE, 1); + break; + case EVENT_START_COUNTDOWN_6: + case EVENT_START_COUNTDOWN_7: + case EVENT_START_COUNTDOWN_8: + case EVENT_START_COUNTDOWN_9: + case EVENT_START_COUNTDOWN_10: + if (eventId == EVENT_START_COUNTDOWN_6) + { + Talk(SAY_LIGHT_OF_DAWN05); + me->CastSpell(me, SPELL_CAMERA_SHAKE_INIT, true); + } + else + me->CastSpell(me, SPELL_CAMERA_SHAKE, true); + me->SummonCreatureGroup(eventId - EVENT_START_COUNTDOWN_6); + break; + case EVENT_START_COUNTDOWN_11: + Talk(SAY_LIGHT_OF_DAWN06); + break; + case EVENT_START_COUNTDOWN_12: + summons.DoAction(ACTION_PLAY_EMOTE); + break; + case EVENT_START_COUNTDOWN_13: + { + uint8 first = 1; + for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) + { + if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) + { + Position pos = LightOfDawnPos[first]; + summon->SetHomePosition(pos); + summon->GetMotionMaster()->MovePoint(1, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, true, false); + } + first = first == 0 ? 1 : 0; + } + Position pos = LightOfDawnPos[first]; + me->SetHomePosition(pos); + me->SetWalk(false); + me->GetMotionMaster()->MovePoint(1, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, true, true); + DoCastSelf(SPELL_THE_MIGHT_OF_MOGRAINE, true); + break; + } + case EVENT_START_COUNTDOWN_14: + me->SetImmuneToAll(false); + me->SummonCreatureGroup(5); + return; + case EVENT_FINISH_FIGHT_1: + summons.DespawnEntry(NPC_DEFENDER_OF_THE_LIGHT); + battleStarted = ENCOUNTER_STATE_OUTRO; + break; + case EVENT_FINISH_FIGHT_2: + { + summons.DespawnEntry(NPC_RAMPAGING_ABOMINATION); + summons.DespawnEntry(NPC_ACHERUS_GHOUL); + summons.DespawnEntry(NPC_WARRIOR_OF_THE_FROZEN_WASTES); + summons.DespawnEntry(NPC_FLESH_BEHEMOTH); + summons.DespawnEntry(NPC_DEFENDER_OF_THE_LIGHT); + + if (Creature* orbaz = GetEntryFromSummons(NPC_ORBAZ_BLOODBANE)) + { + orbaz->SetReactState(REACT_PASSIVE); + orbaz->AI()->Talk(EMOTE_LIGHT_OF_DAWN04); + orbaz->GetMotionMaster()->MovePoint(2, LightOfDawnPos[2], FORCED_MOVEMENT_NONE, 0.f, true, true); + orbaz->DespawnOrUnsummon(7s); + } + + for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) + if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) + { + summon->CombatStop(true); + summon->GetThreatMgr().ClearAllThreat(); + summon->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + summon->SetImmuneToAll(true); + summon->SetReactState(REACT_PASSIVE); + summon->GetMotionMaster()->Clear(false); + } + me->CombatStop(true); + me->GetThreatMgr().ClearAllThreat(); + me->SetImmuneToAll(true); + me->SetReactState(REACT_PASSIVE); + me->GetMotionMaster()->Clear(false); + + // Position main stars + summons.DoAction(ACTION_POSITION_NPCS); + + me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2276.66f, -5273.60f, 81.86f, 5.14f, TEMPSUMMON_CORPSE_DESPAWN); + me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2272.11f, -5279.08f, 82.01f, 5.69f, TEMPSUMMON_CORPSE_DESPAWN); + me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2285.11f, -5276.73f, 82.08f, 4.23f, TEMPSUMMON_CORPSE_DESPAWN); + me->SummonCreature(NPC_DEFENDER_OF_THE_LIGHT, 2290.06f, -5286.41f, 82.51f, 3.16f, TEMPSUMMON_CORPSE_DESPAWN); + break; + } + case EVENT_FINISH_FIGHT_3: + if (Creature* koltira = GetEntryFromSummons(NPC_KOLTIRA_DEATHWEAVER)) + { + koltira->SetWalk(true); + koltira->SetHomePosition(*koltira); + koltira->CastSpell(koltira, SPELL_THE_LIGHT_OF_DAWN, false); + koltira->GetMotionMaster()->MoveCharge(LightOfDawnPos[3].GetPositionX(), LightOfDawnPos[3].GetPositionY(), LightOfDawnPos[3].GetPositionZ(), 4.0f, 2); + } + break; + case EVENT_FINISH_FIGHT_4: + if (Creature* thassarin = GetEntryFromSummons(NPC_THASSARIAN)) + { + thassarin->SetWalk(true); + thassarin->SetHomePosition(*thassarin); + thassarin->CastSpell(thassarin, SPELL_THE_LIGHT_OF_DAWN, false); + thassarin->GetMotionMaster()->MoveCharge(LightOfDawnPos[4].GetPositionX(), LightOfDawnPos[4].GetPositionY(), LightOfDawnPos[4].GetPositionZ(), 4.0f, 2); + } + break; + case EVENT_FINISH_FIGHT_5: + me->SetWalk(true); + me->SetHomePosition(*me); + me->RemoveAllAuras(); + me->CastSpell(me, SPELL_THE_LIGHT_OF_DAWN, false); + me->GetMotionMaster()->MoveCharge(LightOfDawnPos[5].GetPositionX(), LightOfDawnPos[5].GetPositionY(), LightOfDawnPos[5].GetPositionZ(), 4.0f, 2); + + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN26); + break; + case EVENT_OUTRO_SCENE_1: + me->SetStandState(UNIT_STAND_STATE_KNEEL); + me->SetFacingTo(4.8f); + Talk(SAY_LIGHT_OF_DAWN27); + break; + case EVENT_OUTRO_SCENE_2: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN28); + break; + case EVENT_OUTRO_SCENE_3: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN29); + break; + case EVENT_OUTRO_SCENE_4: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN30); + break; + case EVENT_OUTRO_SCENE_5: + me->SetStandState(UNIT_STAND_STATE_STAND); + Talk(SAY_LIGHT_OF_DAWN31); + break; + case EVENT_OUTRO_SCENE_6: + if (Creature* alex = me->SummonCreature(NPC_HIGHLORD_ALEXANDROS_MOGRAINE, LightOfDawnPos[7].GetPositionX(), LightOfDawnPos[7].GetPositionY(), LightOfDawnPos[7].GetPositionZ(), LightOfDawnPos[7].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) + { + alex->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + alex->GetMotionMaster()->MovePoint(0, LightOfDawnPos[8].GetPositionX(), LightOfDawnPos[8].GetPositionY(), LightOfDawnPos[8].GetPositionZ()); + alex->CastSpell(alex, SPELL_ALEXANDROS_MOGRAINE_SPAWN, true); + //alex->AI()->Talk(EMOTE_LIGHT_OF_DAWN06); + } + break; + case EVENT_OUTRO_SCENE_7: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + { + alex->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + alex->AI()->Talk(SAY_LIGHT_OF_DAWN32); + me->SetFacingToObject(alex); + } + break; + case EVENT_OUTRO_SCENE_8: + Talk(SAY_LIGHT_OF_DAWN33); + break; + case EVENT_OUTRO_SCENE_9: + me->SetStandState(UNIT_STAND_STATE_KNEEL); + Talk(SAY_LIGHT_OF_DAWN34); + break; + case EVENT_OUTRO_SCENE_10: + if (Creature* darion = me->SummonCreature(NPC_DARION_MOGRAINE, LightOfDawnPos[9].GetPositionX(), LightOfDawnPos[9].GetPositionY(), LightOfDawnPos[9].GetPositionZ(), LightOfDawnPos[9].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) + { + darion->AI()->Talk(SAY_LIGHT_OF_DAWN35); + darion->SetWalk(false); + } + break; + case EVENT_OUTRO_SCENE_11: + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + { + //darion->AI()->Talk(EMOTE_LIGHT_OF_DAWN07); + darion->GetMotionMaster()->MovePoint(0, LightOfDawnPos[10].GetPositionX(), LightOfDawnPos[10].GetPositionY(), LightOfDawnPos[10].GetPositionZ()); + } + break; + case EVENT_OUTRO_SCENE_12: + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + darion->AI()->Talk(EMOTE_LIGHT_OF_DAWN08); + break; + case EVENT_OUTRO_SCENE_13: + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + darion->AI()->Talk(SAY_LIGHT_OF_DAWN36); + break; + case EVENT_OUTRO_SCENE_14: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + alex->AI()->Talk(SAY_LIGHT_OF_DAWN37); + break; + case EVENT_OUTRO_SCENE_15: + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + darion->AI()->Talk(SAY_LIGHT_OF_DAWN38); + break; + case EVENT_OUTRO_SCENE_16: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + alex->AI()->Talk(SAY_LIGHT_OF_DAWN39); + + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->GetMotionMaster()->MovePoint(0, LightOfDawnPos[11].GetPositionX(), LightOfDawnPos[11].GetPositionY(), LightOfDawnPos[11].GetPositionZ()); + break; + case EVENT_OUTRO_SCENE_17: + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + darion->AI()->Talk(SAY_LIGHT_OF_DAWN40); + break; + case EVENT_OUTRO_SCENE_18: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + alex->AI()->Talk(SAY_LIGHT_OF_DAWN41); + + if (Creature* darion = GetEntryFromSummons(NPC_DARION_MOGRAINE)) + darion->DespawnOrUnsummon(3s); + break; + case EVENT_OUTRO_SCENE_19: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + alex->AI()->Talk(SAY_LIGHT_OF_DAWN42); + + events.Reset(); + events.ScheduleEvent(EVENT_OUTRO_SCENE_20, 4s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_21, 4s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_SCENE_22, 7s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_23, 9s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_24, 14s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_25, 21s + 200ms); + events.ScheduleEvent(EVENT_OUTRO_SCENE_26, 22s + 500ms); + events.ScheduleEvent(EVENT_OUTRO_SCENE_27, 24s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_28, 28s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_29, 34s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_30, 36s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_31, 51s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_32, 68s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_33, 73s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_34, 76s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_35, 77s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_36, 81s); + break; + case EVENT_OUTRO_SCENE_20: + if (Creature* lk = me->SummonCreature(NPC_THE_LICH_KING, LightOfDawnPos[12].GetPositionX(), LightOfDawnPos[12].GetPositionY(), LightOfDawnPos[12].GetPositionZ(), LightOfDawnPos[12].GetOrientation(), TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, 300000)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN43); + break; + case EVENT_OUTRO_SCENE_21: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->CastSpell(lk, SPELL_ICEBOUND_VISAGE, true); + break; + case EVENT_OUTRO_SCENE_22: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + lk->AI()->Talk(SAY_LIGHT_OF_DAWN45); + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + { + alex->RemoveAllAuras(); + lk->CastSpell(alex, SPELL_SOUL_FEAST_ALEX, false); + } + } + break; + case EVENT_OUTRO_SCENE_23: + if (Creature* alex = GetEntryFromSummons(NPC_HIGHLORD_ALEXANDROS_MOGRAINE)) + { + alex->DespawnOrUnsummon(5s); + alex->SetVisible(false); + } + break; + case EVENT_OUTRO_SCENE_24: + me->SetStandState(UNIT_STAND_STATE_STAND); + Talk(SAY_LIGHT_OF_DAWN44); + break; + case EVENT_OUTRO_SCENE_25: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->GetMotionMaster()->MovePoint(0, LightOfDawnPos[13].GetPositionX(), LightOfDawnPos[13].GetPositionY(), LightOfDawnPos[13].GetPositionZ()); + break; + case EVENT_OUTRO_SCENE_26: + me->CastSpell(me, SPELL_MOGRAINE_CHARGE, false); + break; + case EVENT_OUTRO_SCENE_27: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + lk->AI()->Talk(SAY_LIGHT_OF_DAWN46); + // Mograine's charge puts him inside the Lich King's collision, so we need to teleport him out + // Otherwise, Rebuke will kick him the wrong direction + me->NearTeleportTo(2279.7493f, -5258.1f, 80.065f, 4.3419204f); + lk->m_Events.AddEventAtOffset([&, lk] { + lk->CastSpell(me, SPELL_REBUKE, false); + }, 1s); + } + break; + case EVENT_OUTRO_SCENE_28: + me->SetStandState(UNIT_STAND_STATE_KNEEL); + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN47); + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + tirion->SetFacingToObject(lk); + } + break; + case EVENT_OUTRO_SCENE_29: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + lk->HandleEmoteCommand(EMOTE_ONESHOT_LAUGH); + lk->PlayDirectSound(14820); + } + break; + case EVENT_OUTRO_SCENE_30: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN48); + break; + case EVENT_OUTRO_SCENE_31: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN49); + break; + case EVENT_OUTRO_SCENE_32: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + lk->CastSpell(lk, SPELL_SOUL_FEAST_TIRION, false); + tirion->AI()->Talk(EMOTE_LIGHT_OF_DAWN12); + } + + for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) + if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) + if (summon->GetEntry() <= NPC_RIMBLAT_EARTHSHATTER && summon->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING) + { + float o = lk->GetAngle(summon); + summon->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + summon->GetMotionMaster()->MovePoint(3, lk->GetPositionX() + 2.0f * cos(o), lk->GetPositionY() + 2.0f * std::sin(o), lk->GetPositionZ()); + summon->ToTempSummon()->SetTempSummonType(TEMPSUMMON_MANUAL_DESPAWN); + } + } + break; + case EVENT_OUTRO_SCENE_33: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + lk->AI()->Talk(SAY_LIGHT_OF_DAWN51); + lk->CastSpell(lk, SPELL_APOCALYPSE, true); + } + break; + case EVENT_OUTRO_SCENE_34: + for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) + if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) + if (summon->GetEntry() <= NPC_RIMBLAT_EARTHSHATTER && summon->GetEntry() != NPC_HIGHLORD_TIRION_FORDRING) + Unit::Kill(summon, summon); + break; + case EVENT_OUTRO_SCENE_35: + Talk(SAY_LIGHT_OF_DAWN52); + break; + case EVENT_OUTRO_SCENE_36: + me->SetStandState(UNIT_STAND_STATE_STAND); + Talk(SAY_LIGHT_OF_DAWN53); + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + me->SetFacingToObject(tirion); + + events.Reset(); + events.ScheduleEvent(EVENT_OUTRO_SCENE_37, 1s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_38, 5s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_39, 7s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_40, 9s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_41, 13s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_42, 16s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_43, 17s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_44, 19s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_45, 25s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_46, 32s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_47, 42s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_48, 52s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_49, 54s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_50, 58s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_51, 65s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_52, 70s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_53, 84s); + break; + case EVENT_OUTRO_SCENE_37: + me->SetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0, uint32(EQUIP_UNEQUIP)); + me->CastSpell(me, SPELL_THROW_ASHBRINGER, true); + break; + case EVENT_OUTRO_SCENE_38: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->RemoveAllAuras(); + tirion->CastSpell(me, SPELL_REBIRTH_OF_THE_ASHBRINGER, true); + tirion->SummonGameObject(GO_LIGHT_OF_DAWN, tirion->GetPositionX(), tirion->GetPositionY(), tirion->GetPositionZ(), tirion->GetOrientation(), 0, 0, 0, 0, 180); + tirion->LoadEquipment(1, true); + } + me->SetStandState(UNIT_STAND_STATE_DEAD); + break; + case EVENT_OUTRO_SCENE_39: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->RemoveAllAuras(); + tirion->HandleEmoteCommand(EMOTE_ONESHOT_ROAR); + } + break; + case EVENT_OUTRO_SCENE_40: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN54); + break; + case EVENT_OUTRO_SCENE_41: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN55); + break; + case EVENT_OUTRO_SCENE_42: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN56); + break; + case EVENT_OUTRO_SCENE_43: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->CastSpell(tirion, SPELL_TIRION_CHARGE, true); + tirion->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_READY2H); + tirion->SetImmuneToAll(true); + } + break; + case EVENT_OUTRO_SCENE_44: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN57); + break; + case EVENT_OUTRO_SCENE_45: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN58); + break; + case EVENT_OUTRO_SCENE_46: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + lk->AI()->Talk(SAY_LIGHT_OF_DAWN59); + break; + case EVENT_OUTRO_SCENE_47: + if (Creature* lk = GetEntryFromSummons(NPC_THE_LICH_KING)) + { + lk->CastSpell(lk, SPELL_EXIT_TELEPORT_VISUAL, true); + lk->DespawnOrUnsummon(1500ms); + } + + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + float o = me->GetAngle(tirion); + tirion->GetMotionMaster()->MovePoint(4, me->GetPositionX() + 2.0f * cos(o), me->GetPositionY() + 2.0f * std::sin(o), me->GetPositionZ(), FORCED_MOVEMENT_NONE, 0.f, 0.f, false); + tirion->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + tirion->SetFaction(FACTION_FRIENDLY); + } + break; + case EVENT_OUTRO_SCENE_48: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->CastSpell(me, SPELL_LAY_ON_HANDS, false); + me->SetStandState(UNIT_STAND_STATE_KNEEL); + break; + case EVENT_OUTRO_SCENE_49: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN60); + tirion->SetWalk(true); + } + break; + case EVENT_OUTRO_SCENE_50: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->GetMotionMaster()->MovePoint(4, LightOfDawnPos[14].GetPositionX(), LightOfDawnPos[14].GetPositionY(), LightOfDawnPos[14].GetPositionZ()); + break; + case EVENT_OUTRO_SCENE_51: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->GetMotionMaster()->MovePoint(4, LightOfDawnPos[15].GetPositionX(), LightOfDawnPos[15].GetPositionY(), LightOfDawnPos[15].GetPositionZ()); + break; + case EVENT_OUTRO_SCENE_52: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + { + tirion->SetFacingToObject(me); + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN61); + } + break; + case EVENT_OUTRO_SCENE_53: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN62); + + events.Reset(); + events.ScheduleEvent(EVENT_OUTRO_SCENE_54, 6s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_55, 14s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_56, 27s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_57, 37s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_58, 44s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_59, 50s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_60, 63s); + events.ScheduleEvent(EVENT_OUTRO_SCENE_61, 150s); + break; + case EVENT_OUTRO_SCENE_54: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN63); + break; + case EVENT_OUTRO_SCENE_55: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN64); + break; + case EVENT_OUTRO_SCENE_56: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN65); + break; + case EVENT_OUTRO_SCENE_57: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN66); + break; + case EVENT_OUTRO_SCENE_58: + if (Creature* tirion = GetEntryFromSummons(NPC_HIGHLORD_TIRION_FORDRING)) + tirion->AI()->Talk(SAY_LIGHT_OF_DAWN67); + break; + case EVENT_OUTRO_SCENE_59: + Talk(SAY_LIGHT_OF_DAWN68); + me->SetStandState(UNIT_STAND_STATE_STAND); + break; + case EVENT_OUTRO_SCENE_60: + { + Map::PlayerList const& PlayerList = me->GetMap()->GetPlayers(); + if (!PlayerList.IsEmpty()) + { + for (Map::PlayerList::const_iterator i = PlayerList.begin(); i != PlayerList.end(); ++i) + if (i->GetSource()->IsAlive() && me->IsWithinDistInMap(i->GetSource(), 100)) + i->GetSource()->CastSpell(i->GetSource(), SPELL_THE_LIGHT_OF_DAWN_Q, false); + } + me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_GOSSIP | UNIT_NPC_FLAG_QUESTGIVER); + break; + } + case EVENT_OUTRO_SCENE_61: + summons.DespawnAll(); + me->DespawnOrUnsummon(1ms); + events.Reset(); + return; + } + + if (battleStarted != ENCOUNTER_STATE_FIGHT) + return; + + if (!UpdateVictim()) + return; + + switch (eventId) + { + case EVENT_SPELL_ANTI_MAGIC_ZONE: + DoCast(me, SPELL_ANTI_MAGIC_ZONE1); + events.RescheduleEvent(eventId, 30s, 45s); + break; + case EVENT_SPELL_DEATH_STRIKE: + DoCastVictim(SPELL_DEATH_STRIKE); + events.RescheduleEvent(eventId, 5s, 35s); + break; + case EVENT_SPELL_DEATH_EMBRACE: + DoCastVictim(SPELL_DEATH_EMBRACE); + events.RescheduleEvent(eventId, 45s, 60s); + break; + case EVENT_SPELL_UNHOLY_BLIGHT: + DoCast(me, SPELL_UNHOLY_BLIGHT); + events.RescheduleEvent(eventId, 60s); + break; + case EVENT_SPELL_DARION_MOD_DAMAGE: + DoCast(me, SPELL_DARION_MOD_DAMAGE); + Talk(SAY_LIGHT_OF_DAWN09); + events.RescheduleEvent(eventId, 15s, 25s); + break; + } + + DoMeleeAttackIfReady(); + } +}; + +class spell_chapter5_light_of_dawn_aura : public AuraScript +{ + PrepareAuraScript(spell_chapter5_light_of_dawn_aura); + + void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) + { + GetUnitOwner()->Dismount(); + GetUnitOwner()->SetCanFly(true); + GetUnitOwner()->SetDisableGravity(true); + GetUnitOwner()->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); + } + + void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) + { + GetUnitOwner()->SetCanFly(false); + GetUnitOwner()->SetDisableGravity(false); + GetUnitOwner()->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); + GetUnitOwner()->GetMotionMaster()->MoveFall(); + } + + void Register() override + { + OnEffectApply += AuraEffectApplyFn(spell_chapter5_light_of_dawn_aura::OnApply, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); + OnEffectRemove += AuraEffectRemoveFn(spell_chapter5_light_of_dawn_aura::OnRemove, EFFECT_0, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); + } +}; + +/*###### +## Return to Capitals Npcs Behavior +Quest: 58552 - Return to Orgrimmar +Quest: 58533 - Return to Stormwind +######*/ +enum ReturnToCapital +{ + SPELL_RETURN_TO_ORGRIMMAR_APPLE = 58509, + SPELL_RETURN_TO_ORGRIMMAR_BANANA = 58513, + SPELL_RETURN_TO_ORGRIMMAR_SPIT = 58520, + + EMOTE_THROW_APPLE = 2, + EMOTE_THROW_BANANA = 3, + EMOTE_THROW_SPIT = 4, + SAY_INSULT_TO_DK = 5, + + NPC_SW_GUARD = 68, + NPC_ROYAL_GUARD = 1756, + NPC_CITY_PATROLLER = 1976, + NPC_OG_GUARD = 3296, + NPC_KOR_ELITE = 14304, + + TEXT_BROADCAST_COWER = 31670 // "%s cowers in fear." +}; + +uint32 ReturnToCapitalSpells[3] = +{ + 58509, // Apple + 58513, // Banana + 58520 // Spit +}; + +class spell_chapter5_return_to_capital : public SpellScript +{ + PrepareSpellScript(spell_chapter5_return_to_capital); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_RETURN_TO_ORGRIMMAR_APPLE, SPELL_RETURN_TO_ORGRIMMAR_BANANA, SPELL_RETURN_TO_ORGRIMMAR_SPIT}); + } + + void HandleHit(SpellEffIndex /*effIndex*/) + { + Creature* creature = GetHitUnit()->ToCreature(); + Player* player = GetCaster()->ToPlayer(); + uint32 spellId = GetSpellInfo()->Id; + + if (!spellId || !creature || !player || player->IsGameMaster() || !player->IsAlive() || !creature->IsAlive() || creature->IsInCombat()) + return; + + if (creature->HasSpellCooldown(spellId)) + return; + + if (creature->GetEntry() == NPC_SW_GUARD || creature->GetEntry() == NPC_ROYAL_GUARD || creature->GetEntry() == NPC_CITY_PATROLLER || creature->GetEntry() == NPC_OG_GUARD || creature->GetEntry() == NPC_KOR_ELITE) + { + _emote = urand(2,4); + if (creature) + { + creature->PauseMovement(5000); + creature->SetFacingToObject(player, 30s); + + if (roll_chance_i(30)) + { + creature->AI()->Talk(_emote, player); + creature->CastSpell(player, ReturnToCapitalSpells[_emote - 2]); + } + else + { + creature->AI()->Talk(SAY_INSULT_TO_DK, player); + creature->HandleEmoteCommand(RAND(EMOTE_ONESHOT_POINT,EMOTE_ONESHOT_RUDE)); + } + } + } + /*/// @todo: This needs to be further investigated as there are some "guard" npcs, that have civilian flags and non guard npcs should also insult the dk. + else + if (creature->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_CIVILIAN) + { + creature->HandleEmoteCommand(EMOTE_STATE_COWER); // from sniff, emote 431 for a while, then reset (with "%s cowers in fear." text) + creature->PlayDirectSound(14556); // from sniff + if (player) + { + LocaleConstant loc_idx = player->GetSession()->GetSessionDbLocaleIndex(); + if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(TEXT_BROADCAST_COWER)) + creature->TextEmote(bct->GetText(loc_idx, creature->getGender()), creature); + } + } + */ + + creature->AddSpellCooldown(spellId, 0, 30000); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_chapter5_return_to_capital::HandleHit, EFFECT_0, SPELL_EFFECT_DUMMY); + } +private: + uint8 _emote; +}; + +/*#### +## Valkyr Battle Maiden AI +####*/ + +enum Spells_VBM +{ + SPELL_REVIVE = 51918 +}; + +enum Says_VBM +{ + WHISPER_REVIVE = 0 +}; + +struct npc_valkyr_battle_maiden : public PassiveAI +{ + npc_valkyr_battle_maiden(Creature* creature) : PassiveAI(creature) { } + + uint32 FlyBackTimer; + float x, y, z; + uint32 phase; + + void Reset() override + { + me->setActive(true); + me->SetVisible(false); + me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + me->SetCanFly(true); + FlyBackTimer = 500; + phase = 0; + + me->GetPosition(x, y, z); + z += 4.0f; + x -= 3.5f; + y -= 5.0f; + me->GetMotionMaster()->Clear(false); + me->SetPosition(x, y, z, 0.0f); + } + + void UpdateAI(uint32 diff) override + { + if (FlyBackTimer <= diff) + { + Player* player = nullptr; + if (me->IsSummon()) + { + if (Unit * summoner = me->ToTempSummon()->GetSummonerUnit()) + { + player = summoner->ToPlayer(); + } + } + + if (!player) + { + phase = 3; + } + + switch (phase) + { + case 0: + me->SetWalk(false); + me->HandleEmoteCommand(EMOTE_STATE_FLYGRABCLOSED); + FlyBackTimer = 500; + break; + case 1: + if (player) + { + player->GetClosePoint(x, y, z, me->GetObjectSize()); + } + z += 2.5f; + x -= 2.0f; + y -= 1.5f; + me->GetMotionMaster()->MovePoint(0, x, y, z); + if (player) + { + me->SetTarget(player->GetGUID()); + } + me->SetVisible(true); + FlyBackTimer = 4500; + break; + case 2: + if (player && !player->isResurrectRequested()) + { + me->HandleEmoteCommand(EMOTE_ONESHOT_CUSTOM_SPELL_01); + DoCast(player, SPELL_REVIVE, true); + Talk(WHISPER_REVIVE, player); + } + FlyBackTimer = 5000; + break; + case 3: + me->SetVisible(false); + FlyBackTimer = 3000; + break; + case 4: + me->DisappearAndDie(); + break; + default: + //Nothing To DO + break; + } + ++phase; + } + else FlyBackTimer -= diff; + } +}; + +void AddSC_the_scarlet_enclave() +{ + 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); + RegisterSpellScript(spell_death_knight_initiate_visual); + RegisterSpellScript(spell_lich_king_whisper); + RegisterSpellScript(spell_lich_king_vo_blocker); + RegisterSpellScript(spell_chapter2_persuasive_strike); + RegisterSpellScript(spell_portal_effect_acherus); + RegisterSpellScript(spell_q12779_an_end_to_all_things); + RegisterSpellScript(spell_q12779_an_end_to_all_things_devour_aura); + RegisterSpellScript(spell_chapter5_light_of_dawn_aura); + RegisterSpellScript(spell_chapter5_return_to_capital); + RegisterCreatureAI(npc_valkyr_battle_maiden); + RegisterCreatureAI(npc_scarlet_ghoul); + RegisterCreatureAI(npc_dkc1_gothik); + RegisterCreatureAI(npc_scarlet_courier); + RegisterCreatureAI(npc_koltira_deathweaver); + RegisterCreatureAI(npc_a_special_surprise); + RegisterCreatureAI(npc_acherus_necromancer); + RegisterCreatureAI(npc_gothik_the_harvester); + RegisterCreatureAI(npc_highlord_darion_mograine); +} From 95eaad99010264e6f26bdb7f6827a4e6623a60f4 Mon Sep 17 00:00:00 2001 From: sudlud Date: Sun, 26 Jul 2026 14:27:03 +0200 Subject: [PATCH 37/69] fix(Core/Updater): fail the dry run when a SQL update fails to apply (#26795) Co-authored-by: Claude Opus 5 --- .github/workflows/dashboard-ci.yml | 62 ++++++++++++++++++++++- src/server/apps/authserver/Main.cpp | 7 +++ src/server/database/Updater/DBUpdater.cpp | 20 ++++++++ src/server/database/Updater/DBUpdater.h | 7 +++ src/server/game/World/World.cpp | 8 +++ src/tools/dbimport/Main.cpp | 7 +++ 6 files changed, 109 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 263ea07a6..c1fa7151d 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -247,18 +247,36 @@ jobs: run: | ./acore.sh test core + # This job runs zero-conf, so the core falls back to console-only logging and + # writes no Errors.log, which is what the build workflows scrape. The grep is a + # second net in case a later change stops the updater from failing the run on a + # bad import. - name: Test authserver dry-run run: | source ./acore.sh config load cd env/dist/bin - timeout 5m ./authserver -dry-run + timeout 5m ./authserver -dry-run 2>&1 | tee "$RUNNER_TEMP/authserver-dry-run.log" + status=${PIPESTATUS[0]} + [ "$status" -eq 0 ] || exit "$status" + + if grep -qE "Applying of file .+ failed!" "$RUNNER_TEMP/authserver-dry-run.log"; then + echo "::error::A SQL update failed to apply during the authserver dry run" + exit 1 + fi continue-on-error: false - name: Test worldserver dry-run run: | source ./acore.sh config load cd env/dist/bin - timeout 5m ./worldserver -dry-run + timeout 5m ./worldserver -dry-run 2>&1 | tee "$RUNNER_TEMP/worldserver-dry-run.log" + status=${PIPESTATUS[0]} + [ "$status" -eq 0 ] || exit "$status" + + if grep -qE "Applying of file .+ failed!" "$RUNNER_TEMP/worldserver-dry-run.log"; then + echo "::error::A SQL update failed to apply during the worldserver dry run" + exit 1 + fi continue-on-error: false @@ -288,6 +306,46 @@ jobs: timeout-minutes: 30 continue-on-error: false + # Proves the dry-run steps above actually fail on a bad import. + - name: Verify a broken SQL update fails the dry run + run: | + # Absolute: the trap fires after the cd below. + probe="$PWD/data/sql/updates/pending_db_world/rev_9999999999999999999.sql" + trap 'rm -f "$probe"' EXIT + + cat > "$probe" <<'EOF' + DELETE FROM `ci_gate_probe_table_that_does_not_exist` WHERE `id` = 1; + EOF + + source ./acore.sh config load + cd env/dist/bin + + # Without this the step fails on a healthy tree as soon as it gains a + # `shell: bash`: that turns pipefail on, so errexit aborts on the exit 1 the + # assertions below are here to check for. + set +o pipefail + timeout 5m ./worldserver -dry-run 2>&1 | tee "$RUNNER_TEMP/broken-update-dry-run.log" + status=${PIPESTATUS[0]} + + # Exactly 1, so a hang (timeout returns 124) does not pass as a detection. + if [ "$status" -ne 1 ]; then + echo "::error::expected worldserver -dry-run to exit 1 on a broken SQL update, got $status" + exit 1 + fi + + # 1 is also the generic startup failure code, so require the updater to be the one + # that failed the run, on the probe file specifically. + if ! grep -qE "Applying of file .*rev_9999999999999999999\.sql.* failed!" "$RUNNER_TEMP/broken-update-dry-run.log"; then + echo "::error::worldserver -dry-run exited 1 without reporting the broken probe update" + exit 1 + fi + + if ! grep -q "Failed Database Update" "$RUNNER_TEMP/broken-update-dry-run.log"; then + echo "::error::the dry run did not terminate on the failed update count" + exit 1 + fi + timeout-minutes: 10 + # The old "only save on a cache miss" guard is dropped: the per-run key # only ever collides on a re-run, since github.run_id is stable across # attempts, and there the save is a no-op anyway. diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index f5c3b80bc..d19e2b83c 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -27,6 +27,7 @@ #include "AuthSocketMgr.h" #include "Banner.h" #include "Config.h" +#include "DBUpdater.h" #include "DatabaseEnv.h" #include "DatabaseLoader.h" #include "GitRevision.h" @@ -149,6 +150,12 @@ int main(int argc, char** argv) // Stop auth server if dry run if (sConfigMgr->isDryRun()) { + if (uint32 failed = DBUpdaterUtil::GetFailedUpdateCount()) + { + LOG_FATAL("server.authserver", "Dry run completed with {} failed database update(s), terminating.", failed); + return 1; + } + LOG_INFO("server.authserver", "Dry run completed, terminating."); return 0; } diff --git a/src/server/database/Updater/DBUpdater.cpp b/src/server/database/Updater/DBUpdater.cpp index a98cae3ff..554e568d8 100644 --- a/src/server/database/Updater/DBUpdater.cpp +++ b/src/server/database/Updater/DBUpdater.cpp @@ -64,6 +64,22 @@ std::string& DBUpdaterUtil::corrected_path() return path; } +uint32& DBUpdaterUtil::failed_updates() +{ + static uint32 count = 0; + return count; +} + +void DBUpdaterUtil::MarkUpdateFailed() +{ + ++failed_updates(); +} + +uint32 DBUpdaterUtil::GetFailedUpdateCount() +{ + return failed_updates(); +} + // Auth Database template<> std::string DBUpdater::GetConfigEntry() @@ -524,6 +540,10 @@ void DBUpdater::ApplyFile(DatabaseWorkerPool& pool, std::string const& hos "If you are a developer, please fix your sql query.", path.generic_string(), pool.GetConnectionInfo()->database); + // Recorded in both modes. A dry run does not throw below, so it keeps attempting the + // remaining files and this count is the only thing left to fail the run on. + DBUpdaterUtil::MarkUpdateFailed(); + if (!sConfigMgr->isDryRun()) { if (uint32 delay = sConfigMgr->GetOption("Updates.ExceptionShutdownDelay", 10000)) diff --git a/src/server/database/Updater/DBUpdater.h b/src/server/database/Updater/DBUpdater.h index 58ebf3320..506c549c1 100644 --- a/src/server/database/Updater/DBUpdater.h +++ b/src/server/database/Updater/DBUpdater.h @@ -60,8 +60,15 @@ public: static bool CheckExecutable(); + // Counts every update file that failed to apply, in any mode. A dry run does not throw + // on a bad file, so it keeps going and a single run reports all of them; whoever ends + // the run must check this and exit non-zero, otherwise CI goes green on a failed import. + static void MarkUpdateFailed(); + static uint32 GetFailedUpdateCount(); + private: static std::string& corrected_path(); + static uint32& failed_updates(); }; template diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index f68c524c5..ad54e8364 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -42,6 +42,7 @@ #include "CreatureGroups.h" #include "CreatureTextMgr.h" #include "DBCStores.h" +#include "DBUpdater.h" #include "DatabaseEnv.h" #include "DisableMgr.h" #include "DynamicVisibility.h" @@ -1058,6 +1059,13 @@ void World::SetInitialWorldSettings() if (sConfigMgr->isDryRun()) { sMapMgr->UnloadAll(); + + if (uint32 failed = DBUpdaterUtil::GetFailedUpdateCount()) + { + LOG_FATAL("server.loading", "AzerothCore Dry Run Completed With {} Failed Database Update(s), Terminating.", failed); + exit(1); + } + LOG_INFO("server.loading", "AzerothCore Dry Run Completed, Terminating."); exit(0); } diff --git a/src/tools/dbimport/Main.cpp b/src/tools/dbimport/Main.cpp index 06b3ce665..9daa32d17 100644 --- a/src/tools/dbimport/Main.cpp +++ b/src/tools/dbimport/Main.cpp @@ -17,6 +17,7 @@ #include "Banner.h" #include "Config.h" +#include "DBUpdater.h" #include "DatabaseEnv.h" #include "DatabaseLoader.h" #include "IoContext.h" @@ -95,6 +96,12 @@ int main(int argc, char** argv) LOG_INFO("dbimport", "Halting process..."); + if (uint32 failed = DBUpdaterUtil::GetFailedUpdateCount()) + { + LOG_FATAL("dbimport", "{} update file(s) failed to apply!", failed); + return 1; + } + return 0; } From ff1f543a102bdfd12aa51f905f30f1f8a47df8f5 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:07:10 -0300 Subject: [PATCH 38/69] fix(Core/Creature): don't charge corpse decay time on forced despawn of a living creature (#26827) --- src/server/game/Entities/Creature/Creature.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index c2fd3c40b..9d388bb51 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -2172,14 +2172,17 @@ void Creature::ForcedDespawn(Milliseconds timeMSToDespawn, Seconds forceRespawnT if (forceRespawnTimer > 0s) m_respawnDelay = forceRespawnTimer.count(); - if (IsAlive()) + bool const wasAlive = IsAlive(); + + if (wasAlive) setDeathState(DeathState::JustDied, true); // Xinef: Set new respawn time, ignore corpse decay time... - // After setDeathState, m_respawnTime includes m_corpseDelay which we don't - // want for a forced respawn. Override it so RemoveCorpse's max() picks ours. - if (forceRespawnTimer > 0s) - m_respawnTime = GameTime::GetGameTime().count() + forceRespawnTimer.count(); + // setDeathState(JustDied) folds m_corpseDelay into m_respawnTime, but a creature + // despawned while alive never leaves a corpse, so the decay must not be charged. + // Recompute so RemoveCorpse's max() has nothing stale to pick up. + if (forceRespawnTimer > 0s || wasAlive) + m_respawnTime = GameTime::GetGameTime().count() + m_respawnDelay; RemoveCorpse(true); From f23ac046ab406ec5d3c68e3a12aae015ef8365a2 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:27:30 -0300 Subject: [PATCH 39/69] refactor(Scripts/OnyxiasLair): clean up boss_onyxia (#26829) --- .../Kalimdor/OnyxiasLair/boss_onyxia.cpp | 208 ++++++++++-------- .../OnyxiasLair/instance_onyxias_lair.cpp | 2 +- .../Kalimdor/OnyxiasLair/onyxias_lair.h | 6 + 3 files changed, 124 insertions(+), 92 deletions(-) diff --git a/src/server/scripts/Kalimdor/OnyxiasLair/boss_onyxia.cpp b/src/server/scripts/Kalimdor/OnyxiasLair/boss_onyxia.cpp index 334b74adb..1c5391d04 100644 --- a/src/server/scripts/Kalimdor/OnyxiasLair/boss_onyxia.cpp +++ b/src/server/scripts/Kalimdor/OnyxiasLair/boss_onyxia.cpp @@ -84,14 +84,23 @@ enum Phases PHASE_LANDED // Phase 3 - Landed after Airphase - 40% health }; -struct sOnyxMove +// Ids 0-8 are reserved for the OnyxiaMoveData waypoints +enum Points { - uint8 CurrId, DestId; - uint32 spellId; - float x, y, z, o; + POINT_GROUND_SOUTH = 10, + POINT_TAKEOFF = 11, + POINT_PRE_LAND = 12, + POINT_LAND = 13 }; -static sOnyxMove OnyxiaMoveData[] = +struct OnyxiaMove +{ + uint8 CurrId, DestId; + uint32 SpellId; + float X, Y, Z, O; +}; + +static OnyxiaMove const OnyxiaMoveData[] = { {0, 0, 0, -64.496f, -214.906f, -84.4f, 0.0f}, // south ground {1, 5, SPELL_BREATH_S_TO_N, -64.496f, -214.906f, -60.0f, 0.0f}, // south @@ -117,24 +126,25 @@ enum Yells struct boss_onyxia : public BossAI { public: - boss_onyxia(Creature* pCreature) : BossAI(pCreature, DATA_ONYXIA) + boss_onyxia(Creature* creature) : BossAI(creature, DATA_ONYXIA) { Initialize(); } void Initialize() { - CurrentWP = 0; - whelpSpam = false; - whelpCount = 0; - whelpSpamTimer = 0; - bManyWhelpsAvailable = false; + _phase = PHASE_NONE; + _currentWP = 0; + _whelpSpam = false; + _whelpCount = 0; + _whelpSpamTimer = 0; + _manyWhelpsAvailable = false; } void SetPhase(uint8 ph) { events.Reset(); - Phase = ph; + _phase = ph; switch (ph) { case PHASE_GROUNDED: @@ -149,6 +159,8 @@ public: case PHASE_LANDED: events.ScheduleEvent(EVENT_START_PHASE_3, 5s); break; + default: + break; } } @@ -168,12 +180,14 @@ public: { switch (param) { - case -1: - if (bManyWhelpsAvailable) + case ACTION_WHELP_SUMMONED: + if (_manyWhelpsAvailable) { instance->SetData(DATA_WHELP_SUMMONED, 1); } break; + default: + break; } } @@ -191,11 +205,11 @@ public: void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override { - if (me->HealthBelowPctDamaged(65, damage) && Phase == PHASE_GROUNDED) + if (me->HealthBelowPctDamaged(65, damage) && _phase == PHASE_GROUNDED) { SetPhase(PHASE_AIRPHASE); } - else if (me->HealthBelowPctDamaged(40, damage) && Phase == PHASE_AIRPHASE) + else if (me->HealthBelowPctDamaged(40, damage) && _phase == PHASE_AIRPHASE) { me->InterruptNonMeleeSpells(false); SetPhase(PHASE_LANDED); @@ -211,7 +225,7 @@ public: return; } - if (summon->GetEntry() == NPC_ONYXIAN_LAIR_GUARD && Phase < PHASE_AIRPHASE) + if (summon->GetEntry() == NPC_ONYXIAN_LAIR_GUARD && _phase < PHASE_AIRPHASE) { return; } @@ -232,11 +246,11 @@ public: if (id < 9) { - if (id > 0 && Phase == PHASE_AIRPHASE) + if (id > 0 && _phase == PHASE_AIRPHASE) { - me->SetFacingTo(OnyxiaMoveData[id].o); + me->SetFacingTo(OnyxiaMoveData[id].O); me->SetSpeed(MOVE_RUN, 1.6f, false); - CurrentWP = id; + _currentWP = id; events.ScheduleEvent(EVENT_SPELL_FIREBALL_FIRST, 1s); } } @@ -244,50 +258,58 @@ public: { switch (id) { - case 10: - me->SetFacingTo(OnyxiaMoveData[0].o); + case POINT_GROUND_SOUTH: + me->SetFacingTo(OnyxiaMoveData[0].O); events.ScheduleEvent(EVENT_LIFTOFF, 0ms); break; - case 11: - me->SetFacingTo(OnyxiaMoveData[1].o); + case POINT_TAKEOFF: + me->SetFacingTo(OnyxiaMoveData[1].O); events.ScheduleEvent(EVENT_FLY_S_TO_N, 0ms); break; - case 12: - me->SetFacingTo(OnyxiaMoveData[1].o); + case POINT_PRE_LAND: + me->SetFacingTo(OnyxiaMoveData[1].O); events.ScheduleEvent(EVENT_LAND, 0ms); break; - case 13: + case POINT_LAND: me->SetCanFly(false); me->SetDisableGravity(false); me->SetSpeed(MOVE_RUN, me->GetCreatureTemplate()->speed_run, false); events.ScheduleEvent(EVENT_PHASE_3_ATTACK, 0ms); break; + default: + break; } } } - void HandleWhelpSpam(const uint32 diff) + // Summons one whelp at each of the two side caves + void SummonWhelps() { - if (whelpSpam) + float angle = rand_norm() * 2 * M_PI; + float dist = rand_norm() * 4.0f; + me->CastSpell(-33.18f + std::cos(angle) * dist, -258.80f + std::sin(angle) * dist, -89.0f, SPELL_SUMMON_WHELP, true); + me->CastSpell(-32.535f + std::cos(angle) * dist, -170.190f + std::sin(angle) * dist, -89.0f, SPELL_SUMMON_WHELP, true); + } + + void HandleWhelpSpam(uint32 diff) + { + if (_whelpSpam) { - if (whelpCount < 40) + if (_whelpCount < 40) { - whelpSpamTimer -= diff; - if (whelpSpamTimer <= 0) + _whelpSpamTimer -= diff; + if (_whelpSpamTimer <= 0) { - float angle = rand_norm() * 2 * M_PI; - float dist = rand_norm() * 4.0f; - me->CastSpell(-33.18f + cos(angle) * dist, -258.80f + std::sin(angle) * dist, -89.0f, 17646, true); - me->CastSpell(-32.535f + cos(angle) * dist, -170.190f + std::sin(angle) * dist, -89.0f, 17646, true); - whelpCount += 2; - whelpSpamTimer += 600; + SummonWhelps(); + _whelpCount += 2; + _whelpSpamTimer += 600; } } else { - whelpSpam = false; - whelpCount = 0; - whelpSpamTimer = 0; + _whelpSpam = false; + _whelpCount = 0; + _whelpSpamTimer = 0; } } } @@ -319,8 +341,6 @@ public: return; } - DoMeleeAttackIfReady(); - switch (events.ExecuteEvent()) { case EVENT_SPELL_WINGBUFFET: @@ -353,7 +373,7 @@ public: me->SetReactState(REACT_PASSIVE); me->StopMoving(); DoResetThreatList(); - me->GetMotionMaster()->MovePoint(10, OnyxiaMoveData[0].x, OnyxiaMoveData[0].y, OnyxiaMoveData[0].z); + me->GetMotionMaster()->MovePoint(POINT_GROUND_SOUTH, OnyxiaMoveData[0].X, OnyxiaMoveData[0].Y, OnyxiaMoveData[0].Z); break; } case EVENT_LIFTOFF: @@ -364,23 +384,23 @@ public: me->DisableSpline(); me->SetCanFly(true); me->SetDisableGravity(true); - me->SetOrientation(OnyxiaMoveData[0].o); + me->SetOrientation(OnyxiaMoveData[0].O); me->SendMovementFlagUpdate(); - me->GetMotionMaster()->MoveTakeoff(11, OnyxiaMoveData[1].x + 1.0f, OnyxiaMoveData[1].y, OnyxiaMoveData[1].z, 12.0f); - bManyWhelpsAvailable = true; + me->GetMotionMaster()->MoveTakeoff(POINT_TAKEOFF, OnyxiaMoveData[1].X + 1.0f, OnyxiaMoveData[1].Y, OnyxiaMoveData[1].Z, 12.0f); + _manyWhelpsAvailable = true; events.RescheduleEvent(EVENT_END_MANY_WHELPS_TIME, 10s); break; } case EVENT_END_MANY_WHELPS_TIME: - bManyWhelpsAvailable = false; + _manyWhelpsAvailable = false; break; case EVENT_FLY_S_TO_N: { me->SetSpeed(MOVE_RUN, 2.95f, false); - me->GetMotionMaster()->MovePoint(5, OnyxiaMoveData[5].x, OnyxiaMoveData[5].y, OnyxiaMoveData[5].z); + me->GetMotionMaster()->MovePoint(5, OnyxiaMoveData[5].X, OnyxiaMoveData[5].Y, OnyxiaMoveData[5].Z); - whelpSpam = true; + _whelpSpam = true; events.ScheduleEvent(EVENT_WHELP_SPAM, 90s); events.ScheduleEvent(EVENT_SUMMON_LAIR_GUARD, 30s); break; @@ -393,7 +413,7 @@ public: } case EVENT_WHELP_SPAM: { - whelpSpam = true; + _whelpSpam = true; events.Repeat(90s); break; } @@ -401,16 +421,16 @@ public: { Talk(SAY_PHASE_3_TRANS); me->SendMeleeAttackStop(me->GetVictim()); - me->GetMotionMaster()->MoveLand(13, OnyxiaMoveData[0].x + 1.0f, OnyxiaMoveData[0].y, OnyxiaMoveData[0].z, 12.0f); + me->GetMotionMaster()->MoveLand(POINT_LAND, OnyxiaMoveData[0].X + 1.0f, OnyxiaMoveData[0].Y, OnyxiaMoveData[0].Z, 12.0f); DoResetThreatList(); break; } case EVENT_SPELL_FIREBALL_FIRST: { - if (Unit* v = SelectTarget(SelectTargetMethod::Random, 0, 200.0f, true)) + if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 200.0f, true)) { - me->SetFacingToObject(v); - DoCast(v, SPELL_FIREBALL); + me->SetFacingToObject(target); + DoCast(target, SPELL_FIREBALL); } events.ScheduleEvent(EVENT_SPELL_FIREBALL_SECOND, 4s); @@ -418,66 +438,65 @@ public: } case EVENT_SPELL_FIREBALL_SECOND: { - if (Unit* v = SelectTarget(SelectTargetMethod::Random, 0, 200.0f, true)) + if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 200.0f, true)) { - me->SetFacingToObject(v); - DoCast(v, SPELL_FIREBALL); + me->SetFacingToObject(target); + DoCast(target, SPELL_FIREBALL); } - uint8 rand = urand(0, 99); - if (rand < 33) + switch (urand(0, 2)) { - events.ScheduleEvent(EVENT_PHASE_2_STEP_CW, 4s); - } - else if (rand < 66) - { - events.ScheduleEvent(EVENT_PHASE_2_STEP_ACW, 4s); - } - else - { - events.ScheduleEvent(EVENT_PHASE_2_STEP_ACROSS, 4s); + case 0: + events.ScheduleEvent(EVENT_PHASE_2_STEP_CW, 4s); + break; + case 1: + events.ScheduleEvent(EVENT_PHASE_2_STEP_ACW, 4s); + break; + default: + events.ScheduleEvent(EVENT_PHASE_2_STEP_ACROSS, 4s); + break; } break; } case EVENT_PHASE_2_STEP_CW: { - uint8 newWP = CurrentWP + 1; + uint8 newWP = _currentWP + 1; if (newWP > 8) { newWP = 1; } - me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].x, OnyxiaMoveData[newWP].y, OnyxiaMoveData[newWP].z); + me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].X, OnyxiaMoveData[newWP].Y, OnyxiaMoveData[newWP].Z); break; } case EVENT_PHASE_2_STEP_ACW: { - uint8 newWP = CurrentWP - 1; + uint8 newWP = _currentWP - 1; if (newWP < 1) { newWP = 8; } - me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].x, OnyxiaMoveData[newWP].y, OnyxiaMoveData[newWP].z); + me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].X, OnyxiaMoveData[newWP].Y, OnyxiaMoveData[newWP].Z); break; } case EVENT_PHASE_2_STEP_ACROSS: { Talk(EMOTE_BREATH); - me->SetFacingTo(OnyxiaMoveData[CurrentWP].o); - DoCastAOE(OnyxiaMoveData[CurrentWP].spellId); + me->SetFacingTo(OnyxiaMoveData[_currentWP].O); + DoCastAOE(OnyxiaMoveData[_currentWP].SpellId); events.ScheduleEvent(EVENT_SPELL_BREATH, 8250ms); break; } case EVENT_SPELL_BREATH: { - uint8 newWP = OnyxiaMoveData[CurrentWP].DestId; + uint8 newWP = OnyxiaMoveData[_currentWP].DestId; me->SetSpeed(MOVE_RUN, 2.95f, false); - me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].x, OnyxiaMoveData[newWP].y, OnyxiaMoveData[newWP].z); + me->GetMotionMaster()->MovePoint(newWP, OnyxiaMoveData[newWP].X, OnyxiaMoveData[newWP].Y, OnyxiaMoveData[newWP].Z); break; } case EVENT_START_PHASE_3: { me->SetSpeed(MOVE_RUN, 2.95f, false); - me->GetMotionMaster()->MovePoint(12, OnyxiaMoveData[1].x, OnyxiaMoveData[1].y, OnyxiaMoveData[1].z); + me->GetMotionMaster()->MovePoint(POINT_PRE_LAND, OnyxiaMoveData[1].X, OnyxiaMoveData[1].Y, OnyxiaMoveData[1].Z); break; } case EVENT_PHASE_3_ATTACK: @@ -509,40 +528,45 @@ public: } case EVENT_ERUPTION: { - if (Creature* trigger = me->SummonCreature(12758, *me, TEMPSUMMON_TIMED_DESPAWN, 1000)) + if (Creature* trigger = me->SummonCreature(NPC_ONYXIA_TRIGGER, *me, TEMPSUMMON_TIMED_DESPAWN, 1000)) { - trigger->CastSpell(trigger, 17731, false); + trigger->CastSpell(trigger, SPELL_ERUPTION, false); } break; } case EVENT_SUMMON_WHELP: { - float angle = rand_norm() * 2 * M_PI; - float dist = rand_norm() * 4.0f; - me->CastSpell(-33.18f + cos(angle) * dist, -258.80f + std::sin(angle) * dist, -89.0f, 17646, true); - me->CastSpell(-32.535f + cos(angle) * dist, -170.190f + std::sin(angle) * dist, -89.0f, 17646, true); + SummonWhelps(); events.Repeat(30s); break; } + default: + break; } + + DoMeleeAttackIfReady(); } void SpellHitTarget(Unit* target, SpellInfo const* spell) override { - if (target->IsPlayer() && spell->DurationEntry && spell->DurationEntry->ID == 328 && spell->Effects[EFFECT_1].TargetA.GetTarget() == 1 && (spell->Effects[EFFECT_1].Amplitude == 50 || spell->Effects[EFFECT_1].Amplitude == 215)) // Deep Breath + // Deep Breath is a chain of dozens of triggered spells with no shared id, + // so identify a hit by the shape common to all of them + if (target->IsPlayer() && spell->DurationEntry && spell->DurationEntry->ID == 328 + && spell->Effects[EFFECT_1].TargetA.GetTarget() == TARGET_UNIT_CASTER + && (spell->Effects[EFFECT_1].Amplitude == 50 || spell->Effects[EFFECT_1].Amplitude == 215)) { instance->SetData(DATA_DEEP_BREATH_FAILED, 1); } } private: - uint8 Phase; - int8 CurrentWP; + uint8 _phase; + int8 _currentWP; - bool whelpSpam; - uint8 whelpCount; - int32 whelpSpamTimer; - bool bManyWhelpsAvailable; + bool _whelpSpam; + uint8 _whelpCount; + int32 _whelpSpamTimer; + bool _manyWhelpsAvailable; }; struct npc_onyxian_lair_guard : public ScriptedAI @@ -590,6 +614,8 @@ public: events.Repeat(18s, 21s); } break; + default: + break; } if (!me->HasUnitState(UNIT_STATE_CASTING) && me->isAttackReady()) diff --git a/src/server/scripts/Kalimdor/OnyxiasLair/instance_onyxias_lair.cpp b/src/server/scripts/Kalimdor/OnyxiasLair/instance_onyxias_lair.cpp index a9c1ed61c..0218c17f5 100644 --- a/src/server/scripts/Kalimdor/OnyxiasLair/instance_onyxias_lair.cpp +++ b/src/server/scripts/Kalimdor/OnyxiasLair/instance_onyxias_lair.cpp @@ -60,7 +60,7 @@ public: go->CastSpell((Unit*)nullptr, 17646); if (Creature* onyxia = GetCreature(DATA_ONYXIA)) { - onyxia->AI()->DoAction(-1); + onyxia->AI()->DoAction(ACTION_WHELP_SUMMONED); } break; } diff --git a/src/server/scripts/Kalimdor/OnyxiasLair/onyxias_lair.h b/src/server/scripts/Kalimdor/OnyxiasLair/onyxias_lair.h index 7e4bc7c45..0b4c16e8b 100644 --- a/src/server/scripts/Kalimdor/OnyxiasLair/onyxias_lair.h +++ b/src/server/scripts/Kalimdor/OnyxiasLair/onyxias_lair.h @@ -36,9 +36,15 @@ enum eCreatures { NPC_ONYXIA = 10184, NPC_ONYXIAN_WHELP = 11262, + NPC_ONYXIA_TRIGGER = 12758, NPC_ONYXIAN_LAIR_GUARD = 36561, }; +enum eActions +{ + ACTION_WHELP_SUMMONED = -1 +}; + enum eGameObjects { GO_WHELP_SPAWNER = 176510, From 6f0ba8e896dec6e3c7d91e7d851e396c68bc3446 Mon Sep 17 00:00:00 2001 From: Anton Popovichenko Date: Mon, 27 Jul 2026 18:51:04 +0200 Subject: [PATCH 40/69] feat(Core): Add clustering support (#16832) Co-authored-by: 3kynox Co-authored-by: nox Co-authored-by: Ludwig --- conf/dist/config.cmake | 1 + deps/CMakeLists.txt | 2 + deps/PackageList.txt | 4 + deps/libsidecar/CMakeLists.txt | 66 ++++ deps/libsidecar/include/battleground-api.h | 77 ++++ deps/libsidecar/include/events-group.h | 66 ++++ deps/libsidecar/include/events-guild.h | 31 ++ .../include/events-servers-registry.h | 23 ++ deps/libsidecar/include/libsidecar.h | 116 ++++++ deps/libsidecar/include/monitoring.h | 36 ++ .../include/player-interactions-api.h | 42 ++ deps/libsidecar/include/player-items-api.h | 76 ++++ deps/libsidecar/include/player-money-api.h | 43 ++ deps/libsidecar/stub/battleground-api.c | 55 +++ deps/libsidecar/stub/battleground-api.h | 69 ++++ deps/libsidecar/stub/events-group.c | 117 ++++++ deps/libsidecar/stub/events-group.h | 58 +++ deps/libsidecar/stub/events-guild.c | 43 ++ deps/libsidecar/stub/events-guild.h | 23 ++ .../libsidecar/stub/events-servers-registry.c | 15 + .../libsidecar/stub/events-servers-registry.h | 15 + deps/libsidecar/stub/libsidecar.c | 138 +++++++ deps/libsidecar/stub/libsidecar.h | 84 ++++ deps/libsidecar/stub/monitoring.c | 16 + deps/libsidecar/stub/monitoring.h | 28 ++ .../libsidecar/stub/player-interactions-api.c | 31 ++ .../libsidecar/stub/player-interactions-api.h | 34 ++ deps/libsidecar/stub/player-items-api.c | 44 +++ deps/libsidecar/stub/player-items-api.h | 68 ++++ deps/libsidecar/stub/player-money-api.c | 31 ++ deps/libsidecar/stub/player-money-api.h | 35 ++ src/cmake/showoptions.cmake | 6 + src/server/apps/CMakeLists.txt | 3 +- src/server/apps/worldserver/Main.cpp | 16 +- .../apps/worldserver/worldserver.conf.dist | 55 +++ .../Implementation/CharacterDatabase.cpp | 7 + .../Implementation/CharacterDatabase.h | 2 + .../game/Battlegrounds/Battleground.cpp | 21 + src/server/game/Battlegrounds/Battleground.h | 2 +- src/server/game/CMakeLists.txt | 3 +- src/server/game/Entities/Item/Item.cpp | 52 +-- src/server/game/Entities/Object/Object.cpp | 6 +- src/server/game/Entities/Object/Object.h | 1 + .../game/Entities/Object/ObjectGuid.cpp | 16 + src/server/game/Entities/Object/ObjectGuid.h | 13 +- src/server/game/Entities/Player/Player.cpp | 42 +- .../game/Entities/Player/PlayerStorage.cpp | 72 ++-- .../game/Entities/Transport/Transport.cpp | 87 +++- .../game/Entities/Transport/Transport.h | 5 + src/server/game/Groups/Group.cpp | 106 ++++- src/server/game/Groups/Group.h | 5 + src/server/game/Groups/GroupMgr.cpp | 6 +- src/server/game/Handlers/ChannelHandler.cpp | 19 + src/server/game/Handlers/CharacterHandler.cpp | 109 ++--- src/server/game/Handlers/ChatHandler.cpp | 15 +- src/server/game/Handlers/TradeHandler.cpp | 98 ++++- src/server/game/Instances/InstanceSaveMgr.cpp | 155 ++++++++ src/server/game/Instances/InstanceSaveMgr.h | 31 ++ src/server/game/Instances/InstanceScript.cpp | 4 + src/server/game/Maps/Map.cpp | 49 ++- src/server/game/Maps/Map.h | 10 + src/server/game/Maps/MapMgr.cpp | 4 + src/server/game/Maps/TransportMgr.cpp | 3 + src/server/game/Server/Protocol/Opcodes.cpp | 2 + src/server/game/Server/Protocol/Opcodes.h | 4 +- src/server/game/Server/WorldSession.cpp | 124 ++++-- src/server/game/Server/WorldSession.h | 4 +- src/server/game/Server/WorldSocket.cpp | 169 ++++---- src/server/game/TC9Sidecar/AsyncTask.h | 93 +++++ src/server/game/TC9Sidecar/TC9GroupHooks.cpp | 115 ++++++ src/server/game/TC9Sidecar/TC9GroupHooks.h | 40 ++ src/server/game/TC9Sidecar/TC9GrpcHandler.cpp | 374 ++++++++++++++++++ src/server/game/TC9Sidecar/TC9GrpcHandler.h | 50 +++ src/server/game/TC9Sidecar/TC9GuildHooks.cpp | 47 +++ src/server/game/TC9Sidecar/TC9GuildHooks.h | 34 ++ src/server/game/TC9Sidecar/TC9Sidecar.cpp | 268 +++++++++++++ src/server/game/TC9Sidecar/TC9Sidecar.h | 78 ++++ src/server/game/World/World.cpp | 23 +- src/server/game/World/WorldState.cpp | 34 +- src/server/scripts/CMakeLists.txt | 3 +- 80 files changed, 3675 insertions(+), 297 deletions(-) create mode 100644 deps/libsidecar/CMakeLists.txt create mode 100644 deps/libsidecar/include/battleground-api.h create mode 100644 deps/libsidecar/include/events-group.h create mode 100644 deps/libsidecar/include/events-guild.h create mode 100644 deps/libsidecar/include/events-servers-registry.h create mode 100644 deps/libsidecar/include/libsidecar.h create mode 100644 deps/libsidecar/include/monitoring.h create mode 100644 deps/libsidecar/include/player-interactions-api.h create mode 100644 deps/libsidecar/include/player-items-api.h create mode 100644 deps/libsidecar/include/player-money-api.h create mode 100644 deps/libsidecar/stub/battleground-api.c create mode 100644 deps/libsidecar/stub/battleground-api.h create mode 100644 deps/libsidecar/stub/events-group.c create mode 100644 deps/libsidecar/stub/events-group.h create mode 100644 deps/libsidecar/stub/events-guild.c create mode 100644 deps/libsidecar/stub/events-guild.h create mode 100644 deps/libsidecar/stub/events-servers-registry.c create mode 100644 deps/libsidecar/stub/events-servers-registry.h create mode 100644 deps/libsidecar/stub/libsidecar.c create mode 100644 deps/libsidecar/stub/libsidecar.h create mode 100644 deps/libsidecar/stub/monitoring.c create mode 100644 deps/libsidecar/stub/monitoring.h create mode 100644 deps/libsidecar/stub/player-interactions-api.c create mode 100644 deps/libsidecar/stub/player-interactions-api.h create mode 100644 deps/libsidecar/stub/player-items-api.c create mode 100644 deps/libsidecar/stub/player-items-api.h create mode 100644 deps/libsidecar/stub/player-money-api.c create mode 100644 deps/libsidecar/stub/player-money-api.h create mode 100644 src/server/game/TC9Sidecar/AsyncTask.h create mode 100644 src/server/game/TC9Sidecar/TC9GroupHooks.cpp create mode 100644 src/server/game/TC9Sidecar/TC9GroupHooks.h create mode 100644 src/server/game/TC9Sidecar/TC9GrpcHandler.cpp create mode 100644 src/server/game/TC9Sidecar/TC9GrpcHandler.h create mode 100644 src/server/game/TC9Sidecar/TC9GuildHooks.cpp create mode 100644 src/server/game/TC9Sidecar/TC9GuildHooks.h create mode 100644 src/server/game/TC9Sidecar/TC9Sidecar.cpp create mode 100644 src/server/game/TC9Sidecar/TC9Sidecar.h diff --git a/conf/dist/config.cmake b/conf/dist/config.cmake index ce2e079f3..e2f24fc6c 100644 --- a/conf/dist/config.cmake +++ b/conf/dist/config.cmake @@ -107,6 +107,7 @@ option(WITH_STRICT_DATABASE_TYPE_CHECKS "Enable strict checking of database fiel option(WITHOUT_METRICS "Disable metrics reporting (i.e. InfluxDB and Grafana)" 0) option(WITH_DETAILED_METRICS "Enable detailed metrics reporting (i.e. time each session takes to update)" 0) option(TOOL_CONFIG_MERGER "Install the Python config merger tool alongside config files" 0) +option(USE_REAL_LIBSIDECAR "Use real libsidecar and expect that compiled shared lib is in deps/libsidecar folder." 0) CheckApplicationsBuildList() CheckToolsBuildList() diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 869351a37..34c489e5c 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -47,3 +47,5 @@ if (BUILD_TOOLS_MAPS) add_subdirectory(libmpq) add_subdirectory(fkYAML) endif() + +add_subdirectory(libsidecar) diff --git a/deps/PackageList.txt b/deps/PackageList.txt index a0a57b6c3..ce7024616 100644 --- a/deps/PackageList.txt +++ b/deps/PackageList.txt @@ -85,3 +85,7 @@ recastnavigation (Recast is state of the art navigation mesh construction toolse fkYAML (A C++ header-only YAML library) https://github.com/fktn-k/fkYAML Version: 721edb3e1a817e527fd9e1e18a3bea300822522e + +libsidecar (enables interaction with other components when the worldserver is running in cluster mode. It requires building a shared library first and putting it in the corresponding folder.) + https://github.com/walkline/ToCloud9/tree/master/game-server/libsidecar + Version: master diff --git a/deps/libsidecar/CMakeLists.txt b/deps/libsidecar/CMakeLists.txt new file mode 100644 index 000000000..170e0cc3b --- /dev/null +++ b/deps/libsidecar/CMakeLists.txt @@ -0,0 +1,66 @@ +# +# This file is part of the AzerothCore Project. See AUTHORS file for Copyright information +# +# This file is free software; as a special exception the author gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# + +# Use stub implementation only when the real libsidecar is disabled. +if (NOT USE_REAL_LIBSIDECAR) + + file(GLOB sources stub/*.c stub/*.h) + + add_library(libsidecar STATIC ${sources}) + + set_target_properties(libsidecar + PROPERTIES + LINKER_LANGUAGE + CXX + INTERFACE_INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR}/stub) + +else() + add_library(libsidecar SHARED IMPORTED GLOBAL) + + # Determine the correct library extension based on platform + if(WIN32) + set(LIBSIDECAR_EXTENSION "dll") + set(LIBSIDECAR_IMPORT_LIB "libsidecar.lib") + elseif(APPLE) + set(LIBSIDECAR_EXTENSION "dylib") + else() + set(LIBSIDECAR_EXTENSION "so") + endif() + + if(WIN32) + set_target_properties(libsidecar + PROPERTIES + IMPORTED_LOCATION + ${CMAKE_CURRENT_SOURCE_DIR}/libsidecar.${LIBSIDECAR_EXTENSION} + IMPORTED_IMPLIB + ${CMAKE_CURRENT_SOURCE_DIR}/${LIBSIDECAR_IMPORT_LIB} + INTERFACE_INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR}/include) + else() + set_target_properties(libsidecar + PROPERTIES + IMPORTED_LOCATION + ${CMAKE_CURRENT_SOURCE_DIR}/libsidecar.${LIBSIDECAR_EXTENSION} + INTERFACE_INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR}/include + IMPORTED_NO_SONAME + true) + + set_target_properties(libsidecar PROPERTIES + IMPORTED_LOCATION_NOCONFIG + "${CMAKE_INSTALL_RPATH}/libsidecar.${LIBSIDECAR_EXTENSION}" + IMPORTED_SONAME_NOCONFIG + "libsidecar.${LIBSIDECAR_EXTENSION}") + endif() + +endif() diff --git a/deps/libsidecar/include/battleground-api.h b/deps/libsidecar/include/battleground-api.h new file mode 100644 index 000000000..0a9f607bb --- /dev/null +++ b/deps/libsidecar/include/battleground-api.h @@ -0,0 +1,77 @@ +#ifndef __BATTLEGROUND_API__ +#define __BATTLEGROUND_API__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum BattlegroundErrorCode { + BattlegroundErrorCodeNoError = 0, + BattlegroundErrorCodeNoHandler = 1, + BattlegroundErrorFailedToCreateBG = 2, + BattlegroundErrorBattlegroundNotFound = 3, +} BattlegroundErrorCode; + +typedef struct { + uint8_t battlegroundTypeID; + uint32_t arenaType; + bool isRated; + uint32_t mapID; + uint8_t bracketLvl; + uint64_t *hordePlayersToAdd; + int hordePlayersToAddSize; + uint64_t *alliancePlayersToAdd; + int alliancePlayersToAddSize; + uint64_t *randomBGPlayers; + int randomBGPlayersSize; +} BattlegroundStartRequest; + +typedef struct { + int errorCode; + uint64_t instanceID; + uint64_t instanceClientID; +} BattlegroundStartResponse; + +typedef BattlegroundStartResponse (*BattlegroundStartHandler) (BattlegroundStartRequest* request); +void SetBattlegroundStartHandler(BattlegroundStartHandler h); +BattlegroundStartResponse CallBattlegroundStartHandler(BattlegroundStartRequest* request); + +typedef struct { + uint8_t battlegroundTypeID; + uint64_t instanceID; + uint64_t *hordePlayersToAdd; + int hordePlayersToAddSize; + uint64_t *alliancePlayersToAdd; + int alliancePlayersToAddSize; + uint64_t *randomBGPlayers; + int randomBGPlayersSize; +} BattlegroundAddPlayersRequest; + +typedef BattlegroundErrorCode (*BattlegroundAddPlayersHandler) (BattlegroundAddPlayersRequest* request); +void SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h); +BattlegroundErrorCode CallBattlegroundAddPlayersHandler(BattlegroundAddPlayersRequest* request); + +typedef enum BattlegroundJoinCheckErrorCode { + BattlegroundJoinCheckErrorCodeOK = 0, + BattlegroundJoinCheckErrorCodeNoHook = 1, + BattlegroundJoinCheckErrorCodeResponseIsFalse = 2, + BattlegroundJoinCheckErrorCodePlayerNotFound = 3, +} BattlegroundJoinCheckErrorCode; + +typedef BattlegroundJoinCheckErrorCode (*CanPlayerJoinBattlegroundQueueHandler)(uint64_t playerGuid); +void SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h); +BattlegroundJoinCheckErrorCode CallCanPlayerJoinBattlegroundQueueHandler(uint64_t playerGuid); + +typedef BattlegroundJoinCheckErrorCode (*CanPlayerTeleportToBattlegroundHandler)(uint64_t playerGuid); +void SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h); +BattlegroundJoinCheckErrorCode CallCanPlayerTeleportToBattlegroundHandler(uint64_t playerGuid); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/events-group.h b/deps/libsidecar/include/events-group.h new file mode 100644 index 000000000..cf821aa41 --- /dev/null +++ b/deps/libsidecar/include/events-group.h @@ -0,0 +1,66 @@ +#ifndef __EVENT_GROUP__ +#define __EVENT_GROUP__ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum GroupStatus { + GroupHookStatusOK = 0, + GroupHookStatusNoHook = 1 +}; + +typedef struct { + uint32_t guid; + uint64_t leader; + uint8_t lootMethod; + uint64_t looterGuid; + uint8_t lootThreshold; + uint8_t groupType; + uint8_t difficulty; + uint8_t raidDifficulty; + uint64_t masterLooterGuid; + uint64_t *members; + uint8_t membersSize; +} EventObjectGroup; + +typedef void (*OnGroupCreatedHook) (EventObjectGroup *group); +void SetOnGroupCreatedHook(OnGroupCreatedHook h); +int CallOnGroupCreatedHook(EventObjectGroup *group); + +typedef void (*OnGroupMemberAddedHook) (uint32_t guid, uint64_t newMemberGuid); +void SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h); +int CallOnGroupMemberAddedHook(uint32_t guid, uint64_t newMemberGuid); + +typedef void (*OnGroupMemberRemovedHook) (uint32_t guid, uint64_t removedMemberGuid, uint64_t newLeaderGuid); +void SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h); +int CallOnGroupMemberRemovedHook(uint32_t guid, uint64_t removedMemberGuid, uint64_t newLeaderGuid); + +typedef void (*OnGroupDisbandedHook) (uint32_t guid); +void SetOnGroupDisbandedHook(OnGroupDisbandedHook h); +int CallOnGroupDisbandedHook(uint32_t guid); + +typedef void (*OnGroupLootTypeChangedHook) (uint32_t guid, uint8_t lootMethod, uint64_t looter, uint8_t lootThreshold); +void SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h); +int CallOnGroupLootTypeChangedHook(uint32_t guid, uint8_t lootMethod, uint64_t looter, uint8_t lootThreshold); + +typedef void (*OnGroupDungeonDifficultyChangedHook) (uint32_t guid, uint8_t difficulty); +void SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h); +int CallOnGroupDungeonDifficultyChangedHook(uint32_t guid, uint8_t difficulty); + +typedef void (*OnGroupRaidDifficultyChangedHook) (uint32_t guid, uint8_t difficulty); +void SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h); +int CallOnGroupRaidDifficultyChangedHook(uint32_t guid, uint8_t difficulty); + +typedef void (*OnGroupConvertedToRaidHook) (uint32_t guid); +void SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h); +int CallOnGroupConvertedToRaidHook(uint32_t guid); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/events-guild.h b/deps/libsidecar/include/events-guild.h new file mode 100644 index 000000000..e6066f00d --- /dev/null +++ b/deps/libsidecar/include/events-guild.h @@ -0,0 +1,31 @@ +#ifndef __EVENT_GUILD__ +#define __EVENT_GUILD__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum GuildHookStatus { + GuildHookStatusOK = 0, + GuildHookStatusNoHook = 1 +}; + +typedef void (*OnGuildMemberAddedHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h); +int CallOnGuildMemberAddedHook(uint64_t guild_id, uint64_t player_guid); + +typedef void (*OnGuildMemberLeftHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h); +int CallOnGuildMemberLeftHook(uint64_t guild_id, uint64_t player_guid); + +typedef void (*OnGuildMemberRemovedHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h); +int CallOnGuildMemberRemovedHook(uint64_t guild_id, uint64_t player_guid); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/events-servers-registry.h b/deps/libsidecar/include/events-servers-registry.h new file mode 100644 index 000000000..578bc336a --- /dev/null +++ b/deps/libsidecar/include/events-servers-registry.h @@ -0,0 +1,23 @@ +#ifndef __EVENT_SERVERS_REGISTRY__ +#define __EVENT_SERVERS_REGISTRY__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum ServersRegistryStatus { + ServersRegistryHookStatusOK = 0, + ServersRegistryHookStatusNoHook = 1 +}; + +typedef void (*OnMapsReassignedHook) (uint32_t* /*maps_added*/, int /*maps_added_size*/, uint32_t* /*maps_removed*/, int /*maps_removed_size*/); +void SetOnMapsReassignedHook(OnMapsReassignedHook h); +int CallOnMapsReassignedHook(uint32_t* maps_added, int maps_added_size, uint32_t* maps_removed, int maps_removed_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/libsidecar.h b/deps/libsidecar/include/libsidecar.h new file mode 100644 index 000000000..ef477c76d --- /dev/null +++ b/deps/libsidecar/include/libsidecar.h @@ -0,0 +1,116 @@ +#ifndef __LIBSIDECAR_H__ +#define __LIBSIDECAR_H__ + +#include +#include + +/* Export/import decoration for Windows DLL */ +#ifdef _WIN32 + #ifdef TC9_BUILDING_DLL + #define TC9_API __declspec(dllexport) + #else + #define TC9_API __declspec(dllimport) + #endif +#else + #ifdef TC9_BUILDING_DLL + #define TC9_API __attribute__((visibility("default"))) + #else + #define TC9_API + #endif +#endif + +/* Include all API headers */ +#include "battleground-api.h" +#include "events-group.h" +#include "events-guild.h" +#include "events-servers-registry.h" +#include "monitoring.h" +#include "player-interactions-api.h" +#include "player-items-api.h" +#include "player-money-api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Main library functions */ +TC9_API void TC9InitLib(uint16_t port, uint32_t realmID, uint8_t isCrossRealm, char* availableMaps, uint32_t** assignedMaps, int* assignedMapsSize); +TC9_API void TC9GracefulShutdown(); +TC9_API void TC9ProcessGRPCOrHTTPRequests(); +TC9_API void TC9ProcessEventsHooks(); + +/* GUID generation */ +TC9_API uint64_t TC9GetNextAvailableCharacterGuid(int realmID); +TC9_API uint64_t TC9GetNextAvailableItemGuid(int realmID); +TC9_API uint64_t TC9GetNextAvailableInstanceGuid(int realmID); + +/* Map loading notification */ +TC9_API void TC9ReadyToAcceptPlayersFromMaps(uint32_t* maps, int mapsLen); + +/* Generic NATS pub/sub. Payloads are opaque bytes, subjects are arbitrary. + * Subscription callbacks run on the thread draining TC9ProcessEventsHooks + * (the world update thread), not on the NATS delivery thread. Both return + * 0 on success, -1 on failure. + * + * Example — a mod broadcasting and consuming its own events: + * + * // Publish (any thread): + * const char msg[] = "{\"zone\":1519,\"boss\":466}"; + * TC9NatsPublish("mymod.boss.spawned", msg, sizeof(msg) - 1); + * + * // Subscribe once at startup; the handler runs on the world update + * // thread, so it is safe to touch game state from it: + * void OnBossSpawned(const char* subject, const char* payload, int payloadLen) + * { + * std::string data(payload, payloadLen); // payload is not NUL-terminated + * // ... react to the event ... + * } + * TC9NatsSubscribe("mymod.boss.spawned", &OnBossSpawned); + */ +typedef void (*TC9NatsMessageHandler)(const char* subject, const char* payload, int payloadLen); +TC9_API int TC9NatsPublish(const char* subject, const char* payload, int payloadLen); +TC9_API int TC9NatsSubscribe(const char* subject, TC9NatsMessageHandler handler); + +/* Matchmaking notifications */ +TC9_API void TC9PlayerLeftBattleground(uint64_t playerGUID, uint32_t realmID, uint32_t instanceID); +TC9_API void TC9BattlegroundStatusChanged(uint32_t instanceID, uint8_t status); + +/* Event hooks registration */ +TC9_API void TC9SetOnGroupCreatedHook(OnGroupCreatedHook h); +TC9_API void TC9SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h); +TC9_API void TC9SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h); +TC9_API void TC9SetOnGroupDisbandedHook(OnGroupDisbandedHook h); +TC9_API void TC9SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h); +TC9_API void TC9SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h); +TC9_API void TC9SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h); +TC9_API void TC9SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h); + +TC9_API void TC9SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h); +TC9_API void TC9SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h); +TC9_API void TC9SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h); + +TC9_API void TC9SetOnMapsReassignedHook(OnMapsReassignedHook h); + +/* Handler registration for gRPC requests */ +TC9_API void TC9SetBattlegroundStartHandler(BattlegroundStartHandler h); +TC9_API void TC9SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h); +TC9_API void TC9SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h); +TC9_API void TC9SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h); + +TC9_API void TC9SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h); + +TC9_API void TC9SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h); +TC9_API void TC9SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h); + +TC9_API void TC9SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h); +TC9_API void TC9SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h); +TC9_API void TC9SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h); + +TC9_API void TC9SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h); +TC9_API void TC9SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h); + +#ifdef __cplusplus +} +#endif + +#endif /* __LIBSIDECAR_H__ */ diff --git a/deps/libsidecar/include/monitoring.h b/deps/libsidecar/include/monitoring.h new file mode 100644 index 000000000..c87c47721 --- /dev/null +++ b/deps/libsidecar/include/monitoring.h @@ -0,0 +1,36 @@ +#ifndef __MONITORING__ +#define __MONITORING__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum MonitoringErrorCode { + MonitoringErrorCodeNoError = 0, + MonitoringErrorCodeNoHandler = 1, +} MonitoringErrorCode; + +// MonitoringDataCollectorResponse request. +typedef struct { + int errorCode; + uint32_t connectedPlayers; + uint32_t diffMean; + uint32_t diffMedian; + uint32_t diff95Percentile; + uint32_t diff99Percentile; + uint32_t diffMaxPercentile; +} MonitoringDataCollectorResponse; + +typedef MonitoringDataCollectorResponse (*MonitoringDataCollectorHandler)(); +void SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h); +MonitoringDataCollectorResponse CallMonitoringDataCollectorHandler(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/player-interactions-api.h b/deps/libsidecar/include/player-interactions-api.h new file mode 100644 index 000000000..4289b708a --- /dev/null +++ b/deps/libsidecar/include/player-interactions-api.h @@ -0,0 +1,42 @@ +#ifndef __PLAYER_INTERACTIONS_API__ +#define __PLAYER_INTERACTIONS_API__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum PlayerInteractionErrorCode { + PlayerInteractionErrorCodeNoError = 0, + PlayerInteractionErrorCodeNoHandler = 1, + PlayerInteractionErrorCodeCodePlayerNotFound = 2, +} PlayerInteractionErrorCode; + +// CanPlayerInteractWithNPCAndFlags request. +typedef struct { + int errorCode; + bool canInteract; +} CanPlayerInteractWithNPCAndFlagsResponse; + +typedef CanPlayerInteractWithNPCAndFlagsResponse (*CanPlayerInteractWithNPCAndFlagsHandler) (uint64_t /*playerGUID*/, uint64_t /*npcGUID*/, uint32_t /*npcFlags*/); +void SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h); +CanPlayerInteractWithNPCAndFlagsResponse CallCanPlayerInteractWithNPCAndFlagsHandler(uint64_t player_guid, uint64_t npc_guid, uint32_t npc_flags); + +// CanPlayerInteractWithGOAndType request. +typedef struct { + int errorCode; + bool canInteract; +} CanPlayerInteractWithGOAndTypeResponse; + +typedef CanPlayerInteractWithGOAndTypeResponse (*CanPlayerInteractWithGOAndTypeHandler) (uint64_t /*playerGUID*/, uint64_t /*goGUID*/, uint8_t /*goType*/); +void SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h); +CanPlayerInteractWithGOAndTypeResponse CallCanPlayerInteractWithGOAndTypeHandler(uint64_t player_guid, uint64_t go_guid, uint8_t go_type); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/player-items-api.h b/deps/libsidecar/include/player-items-api.h new file mode 100644 index 000000000..22d158bd4 --- /dev/null +++ b/deps/libsidecar/include/player-items-api.h @@ -0,0 +1,76 @@ +#ifndef __PLAYER_ITEMS_API__ +#define __PLAYER_ITEMS_API__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum PlayerItemErrorCode { + PlayerItemErrorCodeNoError = 0, + PlayerItemErrorCodeNoHandler = 1, + PlayerItemErrorCodePlayerNotFound = 2, + PlayerItemErrorNoInventorySpace = 3, + PlayerItemErrorUnknownTemplate = 4, + PlayerItemErrorFailedToCreateItem = 5 +} PlayerItemErrorCode; + +// GetPlayerItemsByGuids request. +typedef struct { + uint64_t guid; + uint32_t entry; + uint64_t owner; + uint8_t bagSlot; + uint8_t slot; + bool isTradable; + uint32_t count; + uint16_t flags; + uint8_t durability; + int8_t randomPropertyID; + const char* text; +} PlayerItem; + +typedef struct { + int errorCode; + PlayerItem* items; + int itemsSize; +} GetPlayerItemsByGuidsResponse; + +typedef GetPlayerItemsByGuidsResponse (*GetPlayerItemsByGuidsHandler) (uint64_t /*player_guid*/, uint64_t* /*items_guids*/, int /*items_guids_size*/); +void SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h); +GetPlayerItemsByGuidsResponse CallGetPlayerItemsByGuidsHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size); + +// RemoveItemsWithGuidsFromPlayer request. +typedef struct { + int errorCode; + uint64_t* updatedItems; + int updatedItemsSize; +} RemoveItemsWithGuidsFromPlayerResponse; + +typedef RemoveItemsWithGuidsFromPlayerResponse (*RemoveItemsWithGuidsFromPlayerHandler) (uint64_t /*player_guid*/, uint64_t* /*items_guids*/, int /*items_guids_size*/, uint64_t /*assign_player_guid*/); +void SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h); +RemoveItemsWithGuidsFromPlayerResponse CallRemoveItemsWithGuidsFromPlayerHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size, uint64_t assign_player_guid); + +// AddExistingItemToPlayer request. +typedef struct { + uint64_t playerGuid; + uint64_t itemGuid; + uint32_t itemEntry; + uint32_t itemCount; + uint16_t itemFlags; + uint8_t itemDurability; + int8_t itemRandomPropertyID; +} AddExistingItemToPlayerRequest; + +typedef PlayerItemErrorCode (*AddExistingItemToPlayerHandler) (AddExistingItemToPlayerRequest*); +void SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h); +PlayerItemErrorCode CallAddExistingItemToPlayerHandler(AddExistingItemToPlayerRequest*); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/include/player-money-api.h b/deps/libsidecar/include/player-money-api.h new file mode 100644 index 000000000..395b881fa --- /dev/null +++ b/deps/libsidecar/include/player-money-api.h @@ -0,0 +1,43 @@ +#ifndef __PLAYER_MONEY_API__ +#define __PLAYER_MONEY_API__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum PlayerMoneyErrorCode { + PlayerMoneyErrorCodeNoError = 0, + PlayerMoneyErrorCodeNoHandler = 1, + PlayerMoneyErrorCodePlayerNotFound = 2, + PlayerMoneyErrorCodeTooMuchMoney = 3, +} PlayerMoneyErrorCode; + +// GetMoneyForPlayer request. +typedef struct { + int errorCode; + uint32_t money; +} GetMoneyForPlayerResponse; + +typedef GetMoneyForPlayerResponse (*GetMoneyForPlayerHandler) (uint64_t /*player_guid*/); +void SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h); +GetMoneyForPlayerResponse CallGetMoneyForPlayerHandler(uint64_t player_guid); + +// ModifyMoneyForPlayer request. +typedef struct { + int errorCode; + uint32_t newMoneyValue; +} ModifyMoneyForPlayerResponse; + +typedef ModifyMoneyForPlayerResponse (*ModifyMoneyForPlayerHandler) (uint64_t /*player_guid*/, int32_t /*amount*/); +void SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h); +ModifyMoneyForPlayerResponse CallModifyMoneyForPlayerHandler(uint64_t player_guid, int32_t amount); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/libsidecar/stub/battleground-api.c b/deps/libsidecar/stub/battleground-api.c new file mode 100644 index 000000000..cde84c658 --- /dev/null +++ b/deps/libsidecar/stub/battleground-api.c @@ -0,0 +1,55 @@ +#include "battleground-api.h" + +static BattlegroundStartHandler battlegroundStartHandler; +void SetBattlegroundStartHandler(BattlegroundStartHandler h) { + battlegroundStartHandler = h; +} + +BattlegroundStartResponse CallBattlegroundStartHandler(BattlegroundStartRequest* request) { + if (battlegroundStartHandler == 0) { + BattlegroundStartResponse resp; + resp.errorCode = BattlegroundErrorCodeNoHandler; + return resp; + } + + return battlegroundStartHandler(request); +} + +static BattlegroundAddPlayersHandler battlegroundAddPlayersHandler; +void SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h) { + battlegroundAddPlayersHandler = h; +} + +BattlegroundErrorCode CallBattlegroundAddPlayersHandler(BattlegroundAddPlayersRequest* request) { + if (battlegroundAddPlayersHandler == 0) { + return BattlegroundErrorCodeNoHandler; + } + + return battlegroundAddPlayersHandler(request); +} + +static CanPlayerJoinBattlegroundQueueHandler canPlayerJoinBattlegroundQueueHandler = 0; + +void SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h) { + canPlayerJoinBattlegroundQueueHandler = h; +} + +BattlegroundJoinCheckErrorCode CallCanPlayerJoinBattlegroundQueueHandler(uint64_t playerGuid) { + if (canPlayerJoinBattlegroundQueueHandler == 0) { + return BattlegroundJoinCheckErrorCodeNoHook; + } + return canPlayerJoinBattlegroundQueueHandler(playerGuid); +} + +static CanPlayerTeleportToBattlegroundHandler canPlayerTeleportToBattlegroundHandler = 0; + +void SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h) { + canPlayerTeleportToBattlegroundHandler = h; +} + +BattlegroundJoinCheckErrorCode CallCanPlayerTeleportToBattlegroundHandler(uint64_t playerGuid) { + if (canPlayerTeleportToBattlegroundHandler == 0) { + return BattlegroundJoinCheckErrorCodeNoHook; + } + return canPlayerTeleportToBattlegroundHandler(playerGuid); +} diff --git a/deps/libsidecar/stub/battleground-api.h b/deps/libsidecar/stub/battleground-api.h new file mode 100644 index 000000000..6134db993 --- /dev/null +++ b/deps/libsidecar/stub/battleground-api.h @@ -0,0 +1,69 @@ +#ifndef __BATTLEGROUND_API__ +#define __BATTLEGROUND_API__ + +#include +#include +#include + +typedef enum BattlegroundErrorCode { + BattlegroundErrorCodeNoError = 0, + BattlegroundErrorCodeNoHandler = 1, + BattlegroundErrorFailedToCreateBG = 2, + BattlegroundErrorBattlegroundNotFound = 3, +} BattlegroundErrorCode; + +typedef struct { + uint8_t battlegroundTypeID; + uint32_t arenaType; + bool isRated; + uint32_t mapID; + uint8_t bracketLvl; + uint64_t *hordePlayersToAdd; + int hordePlayersToAddSize; + uint64_t *alliancePlayersToAdd; + int alliancePlayersToAddSize; + uint64_t *randomBGPlayers; + int randomBGPlayersSize; +} BattlegroundStartRequest; + +typedef struct { + int errorCode; + uint64_t instanceID; + uint64_t instanceClientID; +} BattlegroundStartResponse; + +typedef BattlegroundStartResponse (*BattlegroundStartHandler) (BattlegroundStartRequest* request); +void SetBattlegroundStartHandler(BattlegroundStartHandler h); +BattlegroundStartResponse CallBattlegroundStartHandler(BattlegroundStartRequest* request); + +typedef struct { + uint8_t battlegroundTypeID; + uint64_t instanceID; + uint64_t *hordePlayersToAdd; + int hordePlayersToAddSize; + uint64_t *alliancePlayersToAdd; + int alliancePlayersToAddSize; + uint64_t *randomBGPlayers; + int randomBGPlayersSize; +} BattlegroundAddPlayersRequest; + +typedef BattlegroundErrorCode (*BattlegroundAddPlayersHandler) (BattlegroundAddPlayersRequest* request); +void SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h); +BattlegroundErrorCode CallBattlegroundAddPlayersHandler(BattlegroundAddPlayersRequest* request); + +typedef enum BattlegroundJoinCheckErrorCode { + BattlegroundJoinCheckErrorCodeOK = 0, + BattlegroundJoinCheckErrorCodeNoHook = 1, + BattlegroundJoinCheckErrorCodeResponseIsFalse = 2, + BattlegroundJoinCheckErrorCodePlayerNotFound = 3, +} BattlegroundJoinCheckErrorCode; + +typedef BattlegroundJoinCheckErrorCode (*CanPlayerJoinBattlegroundQueueHandler)(uint64_t playerGuid); +void SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h); +BattlegroundJoinCheckErrorCode CallCanPlayerJoinBattlegroundQueueHandler(uint64_t playerGuid); + +typedef BattlegroundJoinCheckErrorCode (*CanPlayerTeleportToBattlegroundHandler)(uint64_t playerGuid); +void SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h); +BattlegroundJoinCheckErrorCode CallCanPlayerTeleportToBattlegroundHandler(uint64_t playerGuid); + +#endif diff --git a/deps/libsidecar/stub/events-group.c b/deps/libsidecar/stub/events-group.c new file mode 100644 index 000000000..49030bba1 --- /dev/null +++ b/deps/libsidecar/stub/events-group.c @@ -0,0 +1,117 @@ +#include "events-group.h" + +// OnGroupCreatedHook +OnGroupCreatedHook groupCreatedHook; +void SetOnGroupCreatedHook(OnGroupCreatedHook h) { + groupCreatedHook = h; +} + +int CallOnGroupCreatedHook(EventObjectGroup *group) { + if (groupCreatedHook == 0) { + return GroupHookStatusNoHook; + } + groupCreatedHook(group); + return GroupHookStatusOK; +} + +// GroupMemberAdded +static OnGroupMemberAddedHook groupMemberAddedHook; +void SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h) { + groupMemberAddedHook = h; +} + +int CallOnGroupMemberAddedHook(uint32_t guid, uint64_t newMemberGuid) { + if (groupMemberAddedHook == 0) { + return GroupHookStatusNoHook; + } + groupMemberAddedHook(guid, newMemberGuid); + return GroupHookStatusOK; +} + +// GroupMemberRemoved +static OnGroupMemberRemovedHook groupMemberRemovedHook; +void SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h) { + groupMemberRemovedHook = h; +} + +int CallOnGroupMemberRemovedHook(uint32_t guid, uint64_t removedMemberGuid, uint64_t newLeaderGuid) { + if (groupMemberRemovedHook == 0) { + return GroupHookStatusNoHook; + } + groupMemberRemovedHook(guid, removedMemberGuid, newLeaderGuid); + return GroupHookStatusOK; +} + + +typedef void (*OnGroupDisbandedHook) (uint32_t guid); +void SetOnGroupDisbandedHook(OnGroupDisbandedHook h); +int CallOnGroupDisbandedHook(uint32_t guid); + +static OnGroupDisbandedHook groupDisbandedHook; +void SetOnGroupDisbandedHook(OnGroupDisbandedHook h) { + groupDisbandedHook = h; +} + +int CallOnGroupDisbandedHook(uint32_t guid) { + if (groupDisbandedHook == 0) { + return GroupHookStatusNoHook; + } + groupDisbandedHook(guid); + return GroupHookStatusOK; +} + +static OnGroupLootTypeChangedHook groupLootTypeChanged; +void SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h) { + groupLootTypeChanged = h; +} + +int CallOnGroupLootTypeChangedHook(uint32_t guid, uint8_t lootMethod, uint64_t looter, uint8_t lootThreshold) { + if (groupLootTypeChanged == 0) { + return GroupHookStatusNoHook; + } + groupLootTypeChanged(guid, lootMethod, looter, lootThreshold); + return GroupHookStatusOK; +} + +static OnGroupDungeonDifficultyChangedHook groupDungeonDifficultyChanged; +void SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h) { + groupDungeonDifficultyChanged = h; +} + +int CallOnGroupDungeonDifficultyChangedHook(uint32_t guid, uint8_t difficulty) { + if (groupDungeonDifficultyChanged == 0) { + return GroupHookStatusNoHook; + } + groupDungeonDifficultyChanged(guid, difficulty); + return GroupHookStatusOK; +} + +static OnGroupRaidDifficultyChangedHook groupRaidDifficultyChanged; +void SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h) { + groupRaidDifficultyChanged = h; +} + +int CallOnGroupRaidDifficultyChangedHook(uint32_t guid, uint8_t difficulty) { + if (groupRaidDifficultyChanged == 0) { + return GroupHookStatusNoHook; + } + groupRaidDifficultyChanged(guid, difficulty); + return GroupHookStatusOK; +} + +typedef void (*OnGroupConvertedToRaidHook) (uint32_t guid); +void SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h); +int CallOnGroupConvertedToRaidHook(uint32_t guid); + +static OnGroupConvertedToRaidHook groupConvertedToRaid; +void SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h) { + groupConvertedToRaid = h; +} + +int CallOnGroupConvertedToRaidHook(uint32_t guid) { + if (groupConvertedToRaid == 0) { + return GroupHookStatusNoHook; + } + groupConvertedToRaid(guid); + return GroupHookStatusOK; +} diff --git a/deps/libsidecar/stub/events-group.h b/deps/libsidecar/stub/events-group.h new file mode 100644 index 000000000..05b13b3a0 --- /dev/null +++ b/deps/libsidecar/stub/events-group.h @@ -0,0 +1,58 @@ +#ifndef __EVENT_GROUP__ +#define __EVENT_GROUP__ + +#include +#include + +enum GroupStatus { + GroupHookStatusOK = 0, + GroupHookStatusNoHook = 1 +}; + +typedef struct { + uint32_t guid; + uint64_t leader; + uint8_t lootMethod; + uint64_t looterGuid; + uint8_t lootThreshold; + uint8_t groupType; + uint8_t difficulty; + uint8_t raidDifficulty; + uint64_t masterLooterGuid; + uint64_t *members; + uint8_t membersSize; +} EventObjectGroup; + +typedef void (*OnGroupCreatedHook) (EventObjectGroup *group); +void SetOnGroupCreatedHook(OnGroupCreatedHook h); +int CallOnGroupCreatedHook(EventObjectGroup *group); + +typedef void (*OnGroupMemberAddedHook) (uint32_t guid, uint64_t newMemberGuid); +void SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h); +int CallOnGroupMemberAddedHook(uint32_t guid, uint64_t newMemberGuid); + +typedef void (*OnGroupMemberRemovedHook) (uint32_t guid, uint64_t removedMemberGuid, uint64_t newLeaderGuid); +void SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h); +int CallOnGroupMemberRemovedHook(uint32_t guid, uint64_t removedMemberGuid, uint64_t newLeaderGuid); + +typedef void (*OnGroupDisbandedHook) (uint32_t guid); +void SetOnGroupDisbandedHook(OnGroupDisbandedHook h); +int CallOnGroupDisbandedHook(uint32_t guid); + +typedef void (*OnGroupLootTypeChangedHook) (uint32_t guid, uint8_t lootMethod, uint64_t looter, uint8_t lootThreshold); +void SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h); +int CallOnGroupLootTypeChangedHook(uint32_t guid, uint8_t lootMethod, uint64_t looter, uint8_t lootThreshold); + +typedef void (*OnGroupDungeonDifficultyChangedHook) (uint32_t guid, uint8_t difficulty); +void SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h); +int CallOnGroupDungeonDifficultyChangedHook(uint32_t guid, uint8_t difficulty); + +typedef void (*OnGroupRaidDifficultyChangedHook) (uint32_t guid, uint8_t difficulty); +void SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h); +int CallOnGroupRaidDifficultyChangedHook(uint32_t guid, uint8_t difficulty); + +typedef void (*OnGroupConvertedToRaidHook) (uint32_t guid); +void SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h); +int CallOnGroupConvertedToRaidHook(uint32_t guid); + +#endif diff --git a/deps/libsidecar/stub/events-guild.c b/deps/libsidecar/stub/events-guild.c new file mode 100644 index 000000000..728a1b0d5 --- /dev/null +++ b/deps/libsidecar/stub/events-guild.c @@ -0,0 +1,43 @@ +#include "events-guild.h" + +// GuildMemberAddedHook +OnGuildMemberAddedHook guildMemberAddedHook; +void SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h) { + guildMemberAddedHook = h; +} + +int CallOnGuildMemberAddedHook(uint64_t guild_id, uint64_t player_guid) { + if (guildMemberAddedHook == 0) { + return GuildHookStatusNoHook; + } + guildMemberAddedHook(guild_id, player_guid); + return GuildHookStatusOK; +} + +// GuildMemberLeft +static OnGuildMemberLeftHook guildMemberLeftHook; +void SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h) { + guildMemberLeftHook = h; +} + +int CallOnGuildMemberLeftHook(uint64_t guild_id, uint64_t player_guid) { + if (guildMemberLeftHook == 0) { + return GuildHookStatusNoHook; + } + guildMemberLeftHook(guild_id, player_guid); + return GuildHookStatusOK; +} + +// GuildMemberRemoved +static OnGuildMemberRemovedHook guildMemberRemovedHook; +void SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h) { + guildMemberRemovedHook = h; +} + +int CallOnGuildMemberRemovedHook(uint64_t guild_id, uint64_t player_guid) { + if (guildMemberRemovedHook == 0) { + return GuildHookStatusNoHook; + } + guildMemberRemovedHook(guild_id, player_guid); + return GuildHookStatusOK; +} diff --git a/deps/libsidecar/stub/events-guild.h b/deps/libsidecar/stub/events-guild.h new file mode 100644 index 000000000..569389170 --- /dev/null +++ b/deps/libsidecar/stub/events-guild.h @@ -0,0 +1,23 @@ +#ifndef __EVENT_GUILD__ +#define __EVENT_GUILD__ + +#include + +enum GuildHookStatus { + GuildHookStatusOK = 0, + GuildHookStatusNoHook = 1 +}; + +typedef void (*OnGuildMemberAddedHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h); +int CallOnGuildMemberAddedHook(uint64_t guild_id, uint64_t player_guid); + +typedef void (*OnGuildMemberLeftHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h); +int CallOnGuildMemberLeftHook(uint64_t guild_id, uint64_t player_guid); + +typedef void (*OnGuildMemberRemovedHook) (uint64_t /*guild_id*/, uint64_t /*player_guid*/); +void SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h); +int CallOnGuildMemberRemovedHook(uint64_t guild_id, uint64_t player_guid); + +#endif diff --git a/deps/libsidecar/stub/events-servers-registry.c b/deps/libsidecar/stub/events-servers-registry.c new file mode 100644 index 000000000..9856e6437 --- /dev/null +++ b/deps/libsidecar/stub/events-servers-registry.c @@ -0,0 +1,15 @@ +#include "events-servers-registry.h" + +// MapsReassignedHook +OnMapsReassignedHook mapsReassignedHook; +void SetOnMapsReassignedHook(OnMapsReassignedHook h) { + mapsReassignedHook = h; +} + +int CallOnMapsReassignedHook(uint32_t* maps_added, int maps_added_size, uint32_t* maps_removed, int maps_removed_size) { + if (mapsReassignedHook == 0) { + return ServersRegistryHookStatusNoHook; + } + mapsReassignedHook(maps_added, maps_added_size, maps_removed, maps_removed_size); + return ServersRegistryHookStatusOK; +} diff --git a/deps/libsidecar/stub/events-servers-registry.h b/deps/libsidecar/stub/events-servers-registry.h new file mode 100644 index 000000000..04889e0e5 --- /dev/null +++ b/deps/libsidecar/stub/events-servers-registry.h @@ -0,0 +1,15 @@ +#ifndef __EVENT_SERVERS_REGISTRY__ +#define __EVENT_SERVERS_REGISTRY__ + +#include + +enum ServersRegistryStatus { + ServersRegistryHookStatusOK = 0, + ServersRegistryHookStatusNoHook = 1 +}; + +typedef void (*OnMapsReassignedHook) (uint32_t* /*maps_added*/, int /*maps_added_size*/, uint32_t* /*maps_removed*/, int /*maps_removed_size*/); +void SetOnMapsReassignedHook(OnMapsReassignedHook h); +int CallOnMapsReassignedHook(uint32_t* maps_added, int maps_added_size, uint32_t* maps_removed, int maps_removed_size); + +#endif diff --git a/deps/libsidecar/stub/libsidecar.c b/deps/libsidecar/stub/libsidecar.c new file mode 100644 index 000000000..ed13fedd5 --- /dev/null +++ b/deps/libsidecar/stub/libsidecar.c @@ -0,0 +1,138 @@ +#include "libsidecar.h" +#include +#include + +void panicWithTC9Unavailable(const char* message) { + fprintf(stderr, "Tried to call '%s', but using stub for libsidecar. Use -DUSE_REAL_LIBSIDECAR=ON in cmake to use the real one.\n", message); + exit(EXIT_FAILURE); +} + +// TC9SetBattlegroundStartHandler sets handler for starting battleground. +// +extern void TC9SetBattlegroundStartHandler(BattlegroundStartHandler h) { panicWithTC9Unavailable("TC9SetBattlegroundStartHandler"); } + +// TC9SetBattlegroundAddPlayersHandler sets handler for adding players to battleground. +// +extern void TC9SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h) { panicWithTC9Unavailable("TC9SetBattlegroundAddPlayersHandler"); } + +void TC9SetOnGroupCreatedHook(OnGroupCreatedHook h) { panicWithTC9Unavailable("TC9SetOnGroupCreatedHook"); } + +// TC9SetOnGroupMemberAddedHook sets hook for member added event. +// +void TC9SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h) { panicWithTC9Unavailable("TC9SetOnGroupMemberAddedHook"); } + +// TC9SetOnGroupMemberRemovedHook sets hook for member left/kicked event. +// +void TC9SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h) { panicWithTC9Unavailable("TC9SetOnGroupMemberRemovedHook"); } + +// TC9SetOnGroupDisbandedHook sets hook for group disbanded event. +// +void TC9SetOnGroupDisbandedHook(OnGroupDisbandedHook h) { panicWithTC9Unavailable("TC9SetOnGroupDisbandedHook"); } + +// TC9SetOnGroupLootTypeChangedHook sets hook for group loot type changed event. +// +void TC9SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h) { panicWithTC9Unavailable("TC9SetOnGroupLootTypeChangedHook"); } + +// TC9SetOnGroupDungeonDifficultyChangedHook sets hook for group dungeon difficulty changed event. +// +void TC9SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h) { panicWithTC9Unavailable("TC9SetOnGroupDungeonDifficultyChangedHook"); } + +// TC9SetOnGroupRaidDifficultyChangedHook sets hook for group raid difficulty changed event. +// +void TC9SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h) { panicWithTC9Unavailable("TC9SetOnGroupRaidDifficultyChangedHook"); } + +// TC9SetOnGroupConvertedToRaidHook sets hook for group converted to raid event. +// +void TC9SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h) { panicWithTC9Unavailable("TC9SetOnGroupConvertedToRaidHook"); } + +void TC9SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h) { panicWithTC9Unavailable("TC9SetOnGuildMemberAddedHook"); } + +// TC9SetOnGuildMemberRemovedHook sets hook for guild member removed (kicked) event. +void TC9SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h) { panicWithTC9Unavailable("TC9SetOnGuildMemberRemovedHook"); } + +// TC9SetOnGuildMemberLeftHook sets hook for guild member left event. +void TC9SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h) { panicWithTC9Unavailable("TC9SetOnGuildMemberLeftHook"); } + +// TC9ProcessEventsHooks calls all events hooks. +void TC9ProcessEventsHooks() { panicWithTC9Unavailable("TC9ProcessEventsHooks"); } + +// TC9ProcessGRPCOrHTTPRequests calls all grpc or http handlers in queue. +// +void TC9ProcessGRPCOrHTTPRequests() { panicWithTC9Unavailable("TC9ProcessGRPCOrHTTPRequests"); } + +// TC9GetNextAvailableCharacterGuid returns next available characters GUID. Thread unsafe. +uint64_t TC9GetNextAvailableCharacterGuid(int realmID) { panicWithTC9Unavailable("TC9GetNextAvailableCharacterGuid"); return 0; } + +// TC9GetNextAvailableItemGuid returns next available item GUID. Thread unsafe. +uint64_t TC9GetNextAvailableItemGuid(int realmID) { panicWithTC9Unavailable("TC9GetNextAvailableItemGuid"); return 0; } + +// TC9GetNextAvailableInstanceGuid returns next available dungeon/raid instance GUID. Thread unsafe. +uint64_t TC9GetNextAvailableInstanceGuid(int realmID) { panicWithTC9Unavailable("TC9GetNextAvailableInstanceGuid"); return 0; } + +// TC9InitLib inits lib by starting services like grpc and healthcheck. +// Adds game server to the servers registry that will make this server visible for game load balancer. +// +void TC9InitLib(uint16_t port, uint32_t realmID, uint8_t isCrossRealm, char* availableMaps, uint32_t** assignedMaps, int* assignedMapsSize) { panicWithTC9Unavailable("TC9InitLib"); } + +// TC9GracefulShutdown gracefully stops all running services. +// +void TC9GracefulShutdown() { panicWithTC9Unavailable("TC9GracefulShutdown"); } + +// TC9ReadyToAcceptPlayersFromMaps notifies servers registry that this server +// loaded maps related data and ready to accept players from those maps. +// +void TC9ReadyToAcceptPlayersFromMaps(uint32_t* maps, int mapsLen) { panicWithTC9Unavailable("TC9ReadyToAcceptPlayersFromMaps"); } + +// TC9SetCanPlayerInteractWithNPCAndFlagsHandler sets handler for can player interact with NPC and with given NPC flags request. +// +void TC9SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h) { panicWithTC9Unavailable("TC9SetCanPlayerInteractWithNPCAndFlagsHandler"); } + +// TC9SetCanPlayerInteractWithGOAndTypeHandler sets handler for can player interact with GameObject and with given object type request. +// +void TC9SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h) { panicWithTC9Unavailable("TC9SetCanPlayerInteractWithGOAndTypeHandler"); } + +// TC9SetGetPlayerItemsByGuidsHandler sets handler for getting players item by guids request. +void TC9SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h) { panicWithTC9Unavailable("TC9SetGetPlayerItemsByGuidsHandler"); } + +// TC9SetRemoveItemsWithGuidsFromPlayerHandler sets handler for removing items by guids from player request. +void TC9SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h) { panicWithTC9Unavailable("TC9SetRemoveItemsWithGuidsFromPlayerHandler"); } + +// TC9SetAddExistingItemToPlayerHandler sets handler for adding item to player request. +void TC9SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h) { panicWithTC9Unavailable("TC9SetAddExistingItemToPlayerHandler"); } + +// TC9SetGetMoneyForPlayerHandler sets handler for getting money for player request. +// +void TC9SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h) { panicWithTC9Unavailable("TC9SetGetMoneyForPlayerHandler"); } + +// TC9SetModifyMoneyForPlayerHandler sets handler for modify money for given player request. +// +void TC9SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h) { panicWithTC9Unavailable("TC9SetModifyMoneyForPlayerHandler"); } + + +// TC9SetOnMapsReassignedHook sets hook for maps reassigning by servers registry event. +// +void TC9SetOnMapsReassignedHook(OnMapsReassignedHook h) { panicWithTC9Unavailable("TC9SetOnMapsReassignedHook"); } + +// TC9SetMonitoringDataCollectorHandler sets handler for getting data to handle monitoring request. +// +void TC9SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h) { panicWithTC9Unavailable("TC9SetMonitoringDataCollectorHandler"); } + +// TC9NatsPublish/Subscribe generic NATS pub/sub for in-process extensions +int TC9NatsPublish(const char* subject, const char* payload, int payloadLen) { panicWithTC9Unavailable("TC9NatsPublish"); return -1; } +int TC9NatsSubscribe(const char* subject, TC9NatsMessageHandler handler) { panicWithTC9Unavailable("TC9NatsSubscribe"); return -1; } + +// TC9PlayerLeftBattleground notifies matchmaking server that player left battleground +// +void TC9PlayerLeftBattleground(uint64_t playerGUID, uint32_t realmID, uint32_t instanceID) { panicWithTC9Unavailable("TC9PlayerLeftBattleground"); } + +// TC9BattlegroundStatusChanged notifies matchmaking server that battleground status changed +// +void TC9BattlegroundStatusChanged(uint32_t instanceID, uint8_t status) { panicWithTC9Unavailable("TC9BattlegroundStatusChanged"); } + +// TC9SetCanPlayerJoinBattlegroundQueueHandler sets handler for checking if player can join to battleground queue. +// +void TC9SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h) { panicWithTC9Unavailable("TC9SetCanPlayerJoinBattlegroundQueueHandler"); } + +// TC9SetCanPlayerTeleportToBattlegroundHandler sets handler for checking if player can teleport to battleground. +// +void TC9SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h) { panicWithTC9Unavailable("TC9SetCanPlayerTeleportToBattlegroundHandler"); } diff --git a/deps/libsidecar/stub/libsidecar.h b/deps/libsidecar/stub/libsidecar.h new file mode 100644 index 000000000..220d3fa94 --- /dev/null +++ b/deps/libsidecar/stub/libsidecar.h @@ -0,0 +1,84 @@ +#ifndef __LIBSIDECAR_H__ +#define __LIBSIDECAR_H__ + +#include +#include + +/* Include all API headers */ +#include "battleground-api.h" +#include "events-group.h" +#include "events-guild.h" +#include "events-servers-registry.h" +#include "monitoring.h" +#include "player-interactions-api.h" +#include "player-items-api.h" +#include "player-money-api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Main library functions */ +void TC9InitLib(uint16_t port, uint32_t realmID, uint8_t isCrossRealm, char* availableMaps, uint32_t** assignedMaps, int* assignedMapsSize); +void TC9GracefulShutdown(); +void TC9ProcessGRPCOrHTTPRequests(); +void TC9ProcessEventsHooks(); + +/* GUID generation */ +uint64_t TC9GetNextAvailableCharacterGuid(int realmID); +uint64_t TC9GetNextAvailableItemGuid(int realmID); +uint64_t TC9GetNextAvailableInstanceGuid(int realmID); + +/* Map loading notification */ +void TC9ReadyToAcceptPlayersFromMaps(uint32_t* maps, int mapsLen); + +/* Generic NATS pub/sub for in-process extensions. Subscribe callbacks run + * on the thread that calls TC9ProcessEventsHooks. Call after TC9InitLib. + * Both return 0 on success, -1 on error. */ +typedef void (*TC9NatsMessageHandler)(const char* subject, const char* payload, int payloadLen); +int TC9NatsPublish(const char* subject, const char* payload, int payloadLen); +int TC9NatsSubscribe(const char* subject, TC9NatsMessageHandler handler); + +/* Matchmaking notifications */ +void TC9PlayerLeftBattleground(uint64_t playerGUID, uint32_t realmID, uint32_t instanceID); +void TC9BattlegroundStatusChanged(uint32_t instanceID, uint8_t status); + +/* Event hooks registration */ +void TC9SetOnGroupCreatedHook(OnGroupCreatedHook h); +void TC9SetOnGroupMemberAddedHook(OnGroupMemberAddedHook h); +void TC9SetOnGroupMemberRemovedHook(OnGroupMemberRemovedHook h); +void TC9SetOnGroupDisbandedHook(OnGroupDisbandedHook h); +void TC9SetOnGroupLootTypeChangedHook(OnGroupLootTypeChangedHook h); +void TC9SetOnGroupDungeonDifficultyChangedHook(OnGroupDungeonDifficultyChangedHook h); +void TC9SetOnGroupRaidDifficultyChangedHook(OnGroupRaidDifficultyChangedHook h); +void TC9SetOnGroupConvertedToRaidHook(OnGroupConvertedToRaidHook h); + +void TC9SetOnGuildMemberAddedHook(OnGuildMemberAddedHook h); +void TC9SetOnGuildMemberRemovedHook(OnGuildMemberRemovedHook h); +void TC9SetOnGuildMemberLeftHook(OnGuildMemberLeftHook h); + +void TC9SetOnMapsReassignedHook(OnMapsReassignedHook h); + +/* Handler registration for gRPC requests */ +void TC9SetBattlegroundStartHandler(BattlegroundStartHandler h); +void TC9SetBattlegroundAddPlayersHandler(BattlegroundAddPlayersHandler h); +void TC9SetCanPlayerJoinBattlegroundQueueHandler(CanPlayerJoinBattlegroundQueueHandler h); +void TC9SetCanPlayerTeleportToBattlegroundHandler(CanPlayerTeleportToBattlegroundHandler h); + +void TC9SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h); + +void TC9SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h); +void TC9SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h); + +void TC9SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h); +void TC9SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h); +void TC9SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h); + +void TC9SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h); +void TC9SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h); + +#ifdef __cplusplus +} +#endif + +#endif /* __LIBSIDECAR_H__ */ diff --git a/deps/libsidecar/stub/monitoring.c b/deps/libsidecar/stub/monitoring.c new file mode 100644 index 000000000..6d347b3f7 --- /dev/null +++ b/deps/libsidecar/stub/monitoring.c @@ -0,0 +1,16 @@ +#include "monitoring.h" + +static MonitoringDataCollectorHandler monitoringDataCollectorHandler; +void SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h) { + monitoringDataCollectorHandler = h; +} + +MonitoringDataCollectorResponse CallMonitoringDataCollectorHandler() { + if (monitoringDataCollectorHandler == 0) { + MonitoringDataCollectorResponse resp; + resp.errorCode = MonitoringErrorCodeNoHandler; + return resp; + } + + return monitoringDataCollectorHandler(); +} diff --git a/deps/libsidecar/stub/monitoring.h b/deps/libsidecar/stub/monitoring.h new file mode 100644 index 000000000..2cd66b493 --- /dev/null +++ b/deps/libsidecar/stub/monitoring.h @@ -0,0 +1,28 @@ +#ifndef __MONITORING__ +#define __MONITORING__ + +#include +#include +#include + +typedef enum MonitoringErrorCode { + MonitoringErrorCodeNoError = 0, + MonitoringErrorCodeNoHandler = 1, +} MonitoringErrorCode; + +// MonitoringDataCollectorResponse request. +typedef struct { + int errorCode; + uint32_t connectedPlayers; + uint32_t diffMean; + uint32_t diffMedian; + uint32_t diff95Percentile; + uint32_t diff99Percentile; + uint32_t diffMaxPercentile; +} MonitoringDataCollectorResponse; + +typedef MonitoringDataCollectorResponse (*MonitoringDataCollectorHandler)(); +void SetMonitoringDataCollectorHandler(MonitoringDataCollectorHandler h); +MonitoringDataCollectorResponse CallMonitoringDataCollectorHandler(); + +#endif diff --git a/deps/libsidecar/stub/player-interactions-api.c b/deps/libsidecar/stub/player-interactions-api.c new file mode 100644 index 000000000..96c4bab88 --- /dev/null +++ b/deps/libsidecar/stub/player-interactions-api.c @@ -0,0 +1,31 @@ +#include "player-interactions-api.h" + +static CanPlayerInteractWithNPCAndFlagsHandler canPlayerInteractWithNPCAndFlagsHandler; +void SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h) { + canPlayerInteractWithNPCAndFlagsHandler = h; +} + +CanPlayerInteractWithNPCAndFlagsResponse CallCanPlayerInteractWithNPCAndFlagsHandler(uint64_t player_guid, uint64_t npc_guid, uint32_t npc_flags) { + if (canPlayerInteractWithNPCAndFlagsHandler == 0) { + CanPlayerInteractWithNPCAndFlagsResponse resp; + resp.errorCode = PlayerInteractionErrorCodeNoHandler; + return resp; + } + + return canPlayerInteractWithNPCAndFlagsHandler(player_guid, npc_guid, npc_flags); +} + +static CanPlayerInteractWithGOAndTypeHandler canPlayerInteractWithGOAndTypeHandler; +void SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h) { + canPlayerInteractWithGOAndTypeHandler = h; +} + +CanPlayerInteractWithGOAndTypeResponse CallCanPlayerInteractWithGOAndTypeHandler(uint64_t player_guid, uint64_t go_guid, uint8_t go_type) { + if (canPlayerInteractWithGOAndTypeHandler == 0) { + CanPlayerInteractWithGOAndTypeResponse resp; + resp.errorCode = PlayerInteractionErrorCodeNoHandler; + return resp; + } + + return canPlayerInteractWithGOAndTypeHandler(player_guid, go_guid, go_type); +} diff --git a/deps/libsidecar/stub/player-interactions-api.h b/deps/libsidecar/stub/player-interactions-api.h new file mode 100644 index 000000000..f60a916f8 --- /dev/null +++ b/deps/libsidecar/stub/player-interactions-api.h @@ -0,0 +1,34 @@ +#ifndef __PLAYER_INTERACTIONS_API__ +#define __PLAYER_INTERACTIONS_API__ + +#include +#include +#include + +typedef enum PlayerInteractionErrorCode { + PlayerInteractionErrorCodeNoError = 0, + PlayerInteractionErrorCodeNoHandler = 1, + PlayerInteractionErrorCodeCodePlayerNotFound = 2, +} PlayerInteractionErrorCode; + +// CanPlayerInteractWithNPCAndFlags request. +typedef struct { + int errorCode; + bool canInteract; +} CanPlayerInteractWithNPCAndFlagsResponse; + +typedef CanPlayerInteractWithNPCAndFlagsResponse (*CanPlayerInteractWithNPCAndFlagsHandler) (uint64_t /*playerGUID*/, uint64_t /*npcGUID*/, uint32_t /*npcFlags*/); +void SetCanPlayerInteractWithNPCAndFlagsHandler(CanPlayerInteractWithNPCAndFlagsHandler h); +CanPlayerInteractWithNPCAndFlagsResponse CallCanPlayerInteractWithNPCAndFlagsHandler(uint64_t player_guid, uint64_t npc_guid, uint32_t npc_flags); + +// CanPlayerInteractWithGOAndType request. +typedef struct { + int errorCode; + bool canInteract; +} CanPlayerInteractWithGOAndTypeResponse; + +typedef CanPlayerInteractWithGOAndTypeResponse (*CanPlayerInteractWithGOAndTypeHandler) (uint64_t /*playerGUID*/, uint64_t /*goGUID*/, uint8_t /*goType*/); +void SetCanPlayerInteractWithGOAndTypeHandler(CanPlayerInteractWithGOAndTypeHandler h); +CanPlayerInteractWithGOAndTypeResponse CallCanPlayerInteractWithGOAndTypeHandler(uint64_t player_guid, uint64_t go_guid, uint8_t go_type); + +#endif diff --git a/deps/libsidecar/stub/player-items-api.c b/deps/libsidecar/stub/player-items-api.c new file mode 100644 index 000000000..ae992c53f --- /dev/null +++ b/deps/libsidecar/stub/player-items-api.c @@ -0,0 +1,44 @@ +#include "player-items-api.h" + +static GetPlayerItemsByGuidsHandler getPlayerItemsByGuidsHandler; +void SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h) { + getPlayerItemsByGuidsHandler = h; +} + +GetPlayerItemsByGuidsResponse CallGetPlayerItemsByGuidsHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size) { + if (getPlayerItemsByGuidsHandler == 0) { + GetPlayerItemsByGuidsResponse resp; + resp.errorCode = PlayerItemErrorCodeNoHandler; + return resp; + } + + return getPlayerItemsByGuidsHandler(player_guid, items_guids, items_guids_size); +} + +static RemoveItemsWithGuidsFromPlayerHandler removeItemsWithGuidsFromPlayerHandler; +void SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h) { + removeItemsWithGuidsFromPlayerHandler = h; +} + +RemoveItemsWithGuidsFromPlayerResponse CallRemoveItemsWithGuidsFromPlayerHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size, uint64_t assign_player_guid) { + if (removeItemsWithGuidsFromPlayerHandler == 0) { + RemoveItemsWithGuidsFromPlayerResponse resp; + resp.errorCode = PlayerItemErrorCodeNoHandler; + return resp; + } + + return removeItemsWithGuidsFromPlayerHandler(player_guid, items_guids, items_guids_size, assign_player_guid); +} + +static AddExistingItemToPlayerHandler addExistingItemToPlayerHandler; +void SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h) { + addExistingItemToPlayerHandler = h; +} + +PlayerItemErrorCode CallAddExistingItemToPlayerHandler(AddExistingItemToPlayerRequest *r) { + if (addExistingItemToPlayerHandler == 0) { + return PlayerItemErrorCodeNoHandler; + } + + return addExistingItemToPlayerHandler(r); +} diff --git a/deps/libsidecar/stub/player-items-api.h b/deps/libsidecar/stub/player-items-api.h new file mode 100644 index 000000000..8f5dd8ded --- /dev/null +++ b/deps/libsidecar/stub/player-items-api.h @@ -0,0 +1,68 @@ +#ifndef __PLAYER_ITEMS_API__ +#define __PLAYER_ITEMS_API__ + +#include +#include +#include + +typedef enum PlayerItemErrorCode { + PlayerItemErrorCodeNoError = 0, + PlayerItemErrorCodeNoHandler = 1, + PlayerItemErrorCodePlayerNotFound = 2, + PlayerItemErrorNoInventorySpace = 3, + PlayerItemErrorUnknownTemplate = 4, + PlayerItemErrorFailedToCreateItem = 5 +} PlayerItemErrorCode; + +// GetPlayerItemsByGuids request. +typedef struct { + uint64_t guid; + uint32_t entry; + uint64_t owner; + uint8_t bagSlot; + uint8_t slot; + bool isTradable; + uint32_t count; + uint16_t flags; + uint8_t durability; + int8_t randomPropertyID; + const char* text; +} PlayerItem; + +typedef struct { + int errorCode; + PlayerItem* items; + int itemsSize; +} GetPlayerItemsByGuidsResponse; + +typedef GetPlayerItemsByGuidsResponse (*GetPlayerItemsByGuidsHandler) (uint64_t /*player_guid*/, uint64_t* /*items_guids*/, int /*items_guids_size*/); +void SetGetPlayerItemsByGuidsHandler(GetPlayerItemsByGuidsHandler h); +GetPlayerItemsByGuidsResponse CallGetPlayerItemsByGuidsHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size); + +// RemoveItemsWithGuidsFromPlayer request. +typedef struct { + int errorCode; + uint64_t* updatedItems; + int updatedItemsSize; +} RemoveItemsWithGuidsFromPlayerResponse; + +typedef RemoveItemsWithGuidsFromPlayerResponse (*RemoveItemsWithGuidsFromPlayerHandler) (uint64_t /*player_guid*/, uint64_t* /*items_guids*/, int /*items_guids_size*/, uint64_t /*assign_player_guid*/); +void SetRemoveItemsWithGuidsFromPlayerHandler(RemoveItemsWithGuidsFromPlayerHandler h); +RemoveItemsWithGuidsFromPlayerResponse CallRemoveItemsWithGuidsFromPlayerHandler(uint64_t player_guid, uint64_t* items_guids, int items_guids_size, uint64_t assign_player_guid); + +// AddExistingItemToPlayer request. +typedef struct { + uint64_t playerGuid; + uint64_t itemGuid; + uint32_t itemEntry; + uint32_t itemCount; + uint16_t itemFlags; + uint8_t itemDurability; + int8_t itemRandomPropertyID; +} AddExistingItemToPlayerRequest; + +typedef PlayerItemErrorCode (*AddExistingItemToPlayerHandler) (AddExistingItemToPlayerRequest*); +void SetAddExistingItemToPlayerHandler(AddExistingItemToPlayerHandler h); +PlayerItemErrorCode CallAddExistingItemToPlayerHandler(AddExistingItemToPlayerRequest*); + +#endif diff --git a/deps/libsidecar/stub/player-money-api.c b/deps/libsidecar/stub/player-money-api.c new file mode 100644 index 000000000..a7836dbd6 --- /dev/null +++ b/deps/libsidecar/stub/player-money-api.c @@ -0,0 +1,31 @@ +#include "player-money-api.h" + +static GetMoneyForPlayerHandler getMoneyForPlayerHandler; +void SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h) { + getMoneyForPlayerHandler = h; +} + +GetMoneyForPlayerResponse CallGetMoneyForPlayerHandler(uint64_t player_guid) { + if (getMoneyForPlayerHandler == 0) { + GetMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodeNoHandler; + return resp; + } + + return getMoneyForPlayerHandler(player_guid); +} + +static ModifyMoneyForPlayerHandler modifyMoneyForPlayerHandler; +void SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h) { + modifyMoneyForPlayerHandler = h; +} + +ModifyMoneyForPlayerResponse CallModifyMoneyForPlayerHandler(uint64_t player_guid, int32_t amount) { + if (modifyMoneyForPlayerHandler == 0) { + ModifyMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodeNoHandler; + return resp; + } + + return modifyMoneyForPlayerHandler(player_guid, amount); +} diff --git a/deps/libsidecar/stub/player-money-api.h b/deps/libsidecar/stub/player-money-api.h new file mode 100644 index 000000000..dc909c58d --- /dev/null +++ b/deps/libsidecar/stub/player-money-api.h @@ -0,0 +1,35 @@ +#ifndef __PLAYER_MONEY_API__ +#define __PLAYER_MONEY_API__ + +#include +#include +#include + +typedef enum PlayerMoneyErrorCode { + PlayerMoneyErrorCodeNoError = 0, + PlayerMoneyErrorCodeNoHandler = 1, + PlayerMoneyErrorCodePlayerNotFound = 2, + PlayerMoneyErrorCodeTooMuchMoney = 3, +} PlayerMoneyErrorCode; + +// GetMoneyForPlayer request. +typedef struct { + int errorCode; + uint32_t money; +} GetMoneyForPlayerResponse; + +typedef GetMoneyForPlayerResponse (*GetMoneyForPlayerHandler) (uint64_t /*player_guid*/); +void SetGetMoneyForPlayerHandler(GetMoneyForPlayerHandler h); +GetMoneyForPlayerResponse CallGetMoneyForPlayerHandler(uint64_t player_guid); + +// ModifyMoneyForPlayer request. +typedef struct { + int errorCode; + uint32_t newMoneyValue; +} ModifyMoneyForPlayerResponse; + +typedef ModifyMoneyForPlayerResponse (*ModifyMoneyForPlayerHandler) (uint64_t /*player_guid*/, int32_t /*amount*/); +void SetModifyMoneyForPlayerHandler(ModifyMoneyForPlayerHandler h); +ModifyMoneyForPlayerResponse CallModifyMoneyForPlayerHandler(uint64_t player_guid, int32_t amount); + +#endif diff --git a/src/cmake/showoptions.cmake b/src/cmake/showoptions.cmake index 443584127..f8e41d361 100644 --- a/src/cmake/showoptions.cmake +++ b/src/cmake/showoptions.cmake @@ -121,6 +121,12 @@ else() message("* Use GIT revision hash : Yes (default)") endif() +if ( USE_REAL_LIBSIDECAR ) + message("* Use stub for libsidecar : No") +else() + message("* Use stub for libsidecar : Yes") +endif() + if ( NOJEM ) message("") message(" *** NOJEM - WARNING!") diff --git a/src/server/apps/CMakeLists.txt b/src/server/apps/CMakeLists.txt index 188e83093..ebd7f3a65 100644 --- a/src/server/apps/CMakeLists.txt +++ b/src/server/apps/CMakeLists.txt @@ -139,7 +139,8 @@ foreach(APPLICATION_NAME ${APPLICATIONS_BUILD_LIST}) game gsoap readline - gperftools) + gperftools + libsidecar) if (UNIX AND NOT NOJEM) set(${APP_PROJECT_NAME}_LINK_FLAGS "-pthread -lncurses ${${APP_PROJECT_NAME}_LINK_FLAGS}") diff --git a/src/server/apps/worldserver/Main.cpp b/src/server/apps/worldserver/Main.cpp index 511c17e36..e3182d65d 100644 --- a/src/server/apps/worldserver/Main.cpp +++ b/src/server/apps/worldserver/Main.cpp @@ -49,10 +49,12 @@ #include "SharedDefines.h" #include "SteadyTimer.h" #include "Systemd.h" +#include "TC9Sidecar.h" #include "World.h" #include "WorldSessionMgr.h" #include "WorldSocket.h" #include "WorldSocketMgr.h" +#include "libsidecar.h" #include #include #include @@ -365,7 +367,8 @@ int main(int argc, char** argv) sWorldSocketMgr.StopNetwork(); ///- Clean database before leaving - ClearOnlineAccounts(); + if (!sToCloud9Sidecar->ClusterModeEnabled()) + ClearOnlineAccounts(); }); // Set server online (allow connecting now) @@ -397,11 +400,15 @@ int main(int argc, char** argv) cliThread.reset(new std::thread(CliThread), &ShutdownCLIThread); } + sToCloud9Sidecar->Init(worldPort, realm.Id.Realm); + WorldUpdateLoop(); // Shutdown starts here threadPool.reset(); + sToCloud9Sidecar->Deinit(); + sLog->SetSynchronous(); sScriptMgr->OnShutdown(); @@ -455,8 +462,11 @@ bool StartDB() LOG_INFO("server.loading", "Loading World Information..."); LOG_INFO("server.loading", "> RealmID: {}", realm.Id.Realm); - ///- Clean the database before starting - ClearOnlineAccounts(); + ///- Clean the database before starting. + /// Cluster.Enabled is read from config here because sToCloud9Sidecar->Init() + /// has not run yet; ClusterModeEnabled() would still be the default false. + if (!sConfigMgr->GetOption("Cluster.Enabled", false)) + ClearOnlineAccounts(); ///- Insert version info into DB WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_UPD_VERSION); diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 713dbf5fb..d3b9db4c1 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -72,6 +72,8 @@ # DEBUG # DYNAMIC RESPAWN SETTINGS # +# CLUSTER SETTINGS +# ################################################################################################### ################################################################################################### @@ -4893,3 +4895,56 @@ Respawn.ForceCompatibilityMode = 0 # GAME SETTINGS END # # # ################################################################################################### + +################################################################################################### +# # +# CLUSTER SETTINGS BEGIN # +# # +################################################################################################### + +################################################################################################### +# CLUSTER SETTINGS +# +# This feature is experimental and still under development. It enables cluster mode, allowing multiple +# worldservers to run for a single realm, distributing the load between them. Alongside worldservers, +# additional services are required for proper functionality. If you encounter any issues, please report +# them to the ToCloud9 project: https://github.com/walkline/ToCloud9. +# +# Cluster.Enabled +# Description: Enables/disables cluster mode. +# SECURITY: in cluster mode the ToCloud9 gateway is the trusted +# authentication boundary. This worldserver then skips session-key +# digest verification, packet encryption, warden, IP/country locks, +# ban and minimum-security-level enforcement, and the +# character-ownership check on login. The worldserver port must +# only be reachable by the gateway, never directly by players. +# Default: 0 - (Disabled) +# 1 - (Enabled) + +Cluster.Enabled=0 + +# +# Cluster.AvailableMaps +# Description: List of available maps id on this server. +# Examples: "" - (Can handle any map) +# "0,1,573" + +Cluster.AvailableMaps="" + +# +# Cluster.IsCrossrealm +# Description: Enables cross-realm functionality for a cross-realm setup. +# When enabled, a connection to a MySQL cross-realm reverse proxy is required. +# Default: 0 - (Disabled) +# 1 - (Enabled) + +Cluster.IsCrossrealm=0 + +# +################################################################################################### + +################################################################################################### +# # +# CLUSTER SETTINGS END # +# # +################################################################################################### diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index f70764724..9b323b766 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -634,6 +634,13 @@ void CharacterDatabaseConnection::DoPrepareStatements() // world_state PrepareStatement(CHAR_SEL_WORLD_STATE, "SELECT Id, Data FROM world_state", CONNECTION_SYNCH); PrepareStatement(CHAR_REP_WORLD_STATE, "REPLACE INTO world_state (Id, Data) VALUES(?, ?)", CONNECTION_ASYNC); + + // CHAR_NO_OP_PROVIDE_REALM_CONTEXT is a no-op query that accepts a single parameter: the realm ID. + // This query is used specifically in cross-realm scenarios when the database transaction + // lacks sufficient context to determine which realm's database the query should target. + // By providing the realm ID explicitly, this ensures that mysql reverse proxy will use + // correct realm database for the transaction. + PrepareStatement(CHAR_NO_OP_PROVIDE_REALM_CONTEXT, "SELECT ? AS no_op", CONNECTION_ASYNC); } CharacterDatabaseConnection::CharacterDatabaseConnection(MySQLConnectionInfo& connInfo) : MySQLConnection(connInfo) diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index 0ca6e40c9..e9fe3d705 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -543,6 +543,8 @@ enum CharacterDatabaseStatements : uint32 CHAR_SEL_WORLD_STATE, CHAR_REP_WORLD_STATE, + CHAR_NO_OP_PROVIDE_REALM_CONTEXT, + MAX_CHARACTERDATABASE_STATEMENTS }; diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index caa95df99..9937f3441 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -35,10 +35,12 @@ #include "ObjectMgr.h" #include "Pet.h" #include "Player.h" +#include "Realm.h" #include "RBAC.h" #include "ReputationMgr.h" #include "ScriptMgr.h" #include "SpellAuras.h" +#include "TC9Sidecar.h" #include "Transport.h" #include "Util.h" #include "World.h" @@ -277,6 +279,12 @@ void Battleground::Update(uint32 diff) if (!GetInvitedCount(TEAM_HORDE) && !GetInvitedCount(TEAM_ALLIANCE)) { m_SetDeleteThis = true; + + // Only needed for the sidecar notify inside SetStatus; queue and + // spectator code read the status within this manager pass, so do + // not change it on non-cluster servers. + if (sToCloud9Sidecar->ClusterModeEnabled()) + SetStatus(STATUS_WAIT_LEAVE); } return; @@ -1071,6 +1079,11 @@ void Battleground::RemovePlayerAtLeave(Player* player) // if the player was a match participant if (participant) { + if (sToCloud9Sidecar->ClusterModeEnabled()) + sToCloud9Sidecar->OnPlayerLeftBattleground(player->GetGUID().GetCounter(), + player->GetGUID().GetRealmID(), + GetInstanceID()); + player->ClearAfkReports(); WorldPacket data; @@ -1889,3 +1902,11 @@ uint8 Battleground::GetUniqueBracketId() const { return GetMaxLevel() / 10; } + +void Battleground::SetStatus(BattlegroundStatus Status) +{ + m_Status = Status; + + if (sToCloud9Sidecar->ClusterModeEnabled() && GetInstanceID() != 0) + sToCloud9Sidecar->OnBattlegroundStatusChanged(GetInstanceID(), Status); +} diff --git a/src/server/game/Battlegrounds/Battleground.h b/src/server/game/Battlegrounds/Battleground.h index 84b2f1f53..f45a32b5a 100644 --- a/src/server/game/Battlegrounds/Battleground.h +++ b/src/server/game/Battlegrounds/Battleground.h @@ -366,7 +366,7 @@ public: void SetRandomTypeID(BattlegroundTypeId TypeID) { m_RandomTypeID = TypeID; } void SetBracket(PvPDifficultyEntry const* bracketEntry); void SetInstanceID(uint32 InstanceID) { m_InstanceID = InstanceID; } - void SetStatus(BattlegroundStatus Status) { m_Status = Status; } + void SetStatus(BattlegroundStatus Status); void SetClientInstanceID(uint32 InstanceID) { m_ClientInstanceID = InstanceID; } void SetStartTime(uint32 Time) { m_StartTime = Time; } void SetEndTime(uint32 Time) { m_EndTime = Time; } diff --git a/src/server/game/CMakeLists.txt b/src/server/game/CMakeLists.txt index 3ac1d0d35..d1ae863c9 100644 --- a/src/server/game/CMakeLists.txt +++ b/src/server/game/CMakeLists.txt @@ -54,7 +54,8 @@ target_link_libraries(game PRIVATE acore-core-interface PUBLIC - game-interface) + game-interface + libsidecar) set_target_properties(game PROPERTIES diff --git a/src/server/game/Entities/Item/Item.cpp b/src/server/game/Entities/Item/Item.cpp index 2dc3047e2..7b5b34e06 100644 --- a/src/server/game/Entities/Item/Item.cpp +++ b/src/server/game/Entities/Item/Item.cpp @@ -26,6 +26,7 @@ #include "SpellInfo.h" #include "SpellMgr.h" #include "StringConvert.h" +#include "TC9Sidecar.h" #include "Tokenize.h" #include "WorldPacket.h" @@ -348,7 +349,7 @@ void Item::SaveToDB(CharacterDatabaseTransaction trans) uint8 index = 0; CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(uState == ITEM_NEW ? CHAR_REP_ITEM_INSTANCE : CHAR_UPD_ITEM_INSTANCE); stmt->SetData( index, GetEntry()); - stmt->SetData(++index, GetOwnerGUID().GetCounter()); + stmt->SetData(++index, GetOwnerGUID().GetRawValue()); stmt->SetData(++index, GetGuidValue(ITEM_FIELD_CREATOR).GetCounter()); stmt->SetData(++index, GetGuidValue(ITEM_FIELD_GIFTCREATOR).GetCounter()); stmt->SetData(++index, GetCount()); @@ -381,7 +382,7 @@ void Item::SaveToDB(CharacterDatabaseTransaction trans) if ((uState == ITEM_CHANGED) && IsWrapped()) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_GIFT_OWNER); - stmt->SetData(0, GetOwnerGUID().GetCounter()); + stmt->SetData(0, GetOwnerGUID().GetRawValue()); stmt->SetData(1, guid); trans->Append(stmt); } @@ -1096,29 +1097,32 @@ Item* Item::CreateItem(uint32 item, uint32 count, Player const* player, bool clo return nullptr; //don't create item at zero count ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(item); - if (pProto) - { - if (count > pProto->GetMaxStackSize()) - count = pProto->GetMaxStackSize(); - - ASSERT_NODEBUGINFO(count != 0 && "pProto->Stackable == 0 but checked at loading already"); - - Item* pItem = NewItemOrBag(pProto); - if (pItem->Create(sObjectMgr->GetGenerator().Generate(), item, player)) - { - pItem->SetCount(count); - if (!clone) - pItem->SetItemRandomProperties(randomPropertyId ? randomPropertyId : Item::GenerateItemRandomPropertyId(item)); - else if (randomPropertyId) - pItem->SetItemRandomProperties(randomPropertyId); - return pItem; - } - else - delete pItem; - } - else + if (!pProto) ABORT(); - return nullptr; + + if (count > pProto->GetMaxStackSize()) + count = pProto->GetMaxStackSize(); + + ASSERT_NODEBUGINFO(count != 0 && "pProto->Stackable == 0 but checked at loading already"); + + uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID; + if (sToCloud9Sidecar->IsCrossrealm() && player) + realmId = player->GetGUID().GetRealmID(); + + Item* pItem = NewItemOrBag(pProto); + if (!pItem->Create(sObjectMgr->GetGenerator().Generate(realmId), item, player)) + { + delete pItem; + return nullptr; + } + + pItem->SetCount(count); + if (!clone) + pItem->SetItemRandomProperties(randomPropertyId ? randomPropertyId : Item::GenerateItemRandomPropertyId(item)); + else if (randomPropertyId) + pItem->SetItemRandomProperties(randomPropertyId); + + return pItem; } Item* Item::CloneItem(uint32 count, Player const* player) const diff --git a/src/server/game/Entities/Object/Object.cpp b/src/server/game/Entities/Object/Object.cpp index abbae3d26..9bdd47d4a 100644 --- a/src/server/game/Entities/Object/Object.cpp +++ b/src/server/game/Entities/Object/Object.cpp @@ -121,10 +121,14 @@ void Object::_InitValues() } void Object::_Create(ObjectGuid::LowType guidlow, uint32 entry, HighGuid guidhigh) +{ + _Create(ObjectGuid(guidhigh, entry, guidlow)); +} + +void Object::_Create(ObjectGuid guid) { if (!m_uint32Values) _InitValues(); - ObjectGuid guid(guidhigh, entry, guidlow); SetGuidValue(OBJECT_FIELD_GUID, guid); SetUInt32Value(OBJECT_FIELD_TYPE, m_objectType); m_PackGUID.Set(guid); diff --git a/src/server/game/Entities/Object/Object.h b/src/server/game/Entities/Object/Object.h index 8d57e1d0d..ce311a2ef 100644 --- a/src/server/game/Entities/Object/Object.h +++ b/src/server/game/Entities/Object/Object.h @@ -241,6 +241,7 @@ protected: void _InitValues(); void _Create(ObjectGuid::LowType guidlow, uint32 entry, HighGuid guidhigh); + void _Create(ObjectGuid guid); [[nodiscard]] std::string _ConcatFields(uint16 startIndex, uint16 size) const; bool _LoadIntoDataField(std::string const& data, uint32 startOffset, uint32 count); diff --git a/src/server/game/Entities/Object/ObjectGuid.cpp b/src/server/game/Entities/Object/ObjectGuid.cpp index 2a6e23072..b6e12b6e2 100644 --- a/src/server/game/Entities/Object/ObjectGuid.cpp +++ b/src/server/game/Entities/Object/ObjectGuid.cpp @@ -17,6 +17,7 @@ #include "ObjectGuid.h" #include "Log.h" +#include "TC9Sidecar.h" #include "World.h" #include #include @@ -96,6 +97,21 @@ void ObjectGuidGeneratorBase::HandleCounterOverflow(HighGuid high) World::StopNow(ERROR_EXIT_CODE); } +bool ObjectGuidGeneratorBase::GetClusterGuid(HighGuid high, uint16 realmId, ObjectGuid::LowType& clusterGuid) +{ + if (!sToCloud9Sidecar->ClusterModeEnabled()) + return false; + + if (high == HighGuid::Player) + clusterGuid = ObjectGuid::LowType(sToCloud9Sidecar->GenerateCharacterGuid(realmId)); + else if (high == HighGuid::Item) + clusterGuid = ObjectGuid::LowType(sToCloud9Sidecar->GenerateItemGuid(realmId)); + else + return false; + + return true; +} + #define GUID_TRAIT_INSTANTIATE_GUID( HIGH_GUID ) \ template class ObjectGuidGenerator< HIGH_GUID >; diff --git a/src/server/game/Entities/Object/ObjectGuid.h b/src/server/game/Entities/Object/ObjectGuid.h index f92229e99..a32bb2fc9 100644 --- a/src/server/game/Entities/Object/ObjectGuid.h +++ b/src/server/game/Entities/Object/ObjectGuid.h @@ -27,6 +27,9 @@ #include #include +// Realm id packed into bits 32-47 of a player ObjectGuid; 0 means local / non-crossrealm. +constexpr uint16 DEFAULT_NON_CROSSREALM_REALM_ID = 0; + enum TypeID { TYPEID_OBJECT = 0, @@ -142,6 +145,7 @@ class ObjectGuid [[nodiscard]] uint64 GetRawValue() const { return _guid; } [[nodiscard]] HighGuid GetHigh() const { return HighGuid((_guid >> 48) & 0x0000FFFF); } [[nodiscard]] uint32 GetEntry() const { return HasEntry() ? uint32((_guid >> 24) & UI64LIT(0x0000000000FFFFFF)) : 0; } + [[nodiscard]] uint16 GetRealmID() const { return IsPlayer() ? uint16((_guid >> 32) & UI64LIT(0xFFFF)) : 0; } [[nodiscard]] LowType GetCounter() const { return HasEntry() @@ -283,12 +287,13 @@ public: ObjectGuidGeneratorBase(ObjectGuid::LowType start = 1) : _nextGuid(start) { } virtual void Set(ObjectGuid::LowType val) { _nextGuid = val; } - virtual ObjectGuid::LowType Generate() = 0; + virtual ObjectGuid::LowType Generate(uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID) = 0; [[nodiscard]] ObjectGuid::LowType GetNextAfterMaxUsed() const { return _nextGuid; } virtual ~ObjectGuidGeneratorBase() = default; protected: static void HandleCounterOverflow(HighGuid high); + static bool GetClusterGuid(HighGuid high, uint16 realmId, ObjectGuid::LowType& clusterGuid); ObjectGuid::LowType _nextGuid; }; @@ -298,8 +303,12 @@ class ObjectGuidGenerator : public ObjectGuidGeneratorBase public: explicit ObjectGuidGenerator(ObjectGuid::LowType start = 1) : ObjectGuidGeneratorBase(start) { } - ObjectGuid::LowType Generate() override + ObjectGuid::LowType Generate(uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID) override { + ObjectGuid::LowType clusterGuid = 0; + if (GetClusterGuid(high, realmId, clusterGuid)) + return clusterGuid; + if (_nextGuid >= ObjectGuid::GetMaxCounter(high) - 1) HandleCounterOverflow(high); diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 5dc4d9ae9..aede63b5f 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -3783,7 +3783,7 @@ void Player::_LoadSpellCooldowns(PreparedQueryResult result) void Player::_SaveSpellCooldowns(CharacterDatabaseTransaction trans, bool logout) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SPELL_COOLDOWN); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); time_t curTime = GameTime::GetGameTime().count(); @@ -4770,7 +4770,7 @@ void Player::SpawnCorpseBones(bool triggerSave /*= true*/) // pussywizard: update only ghost flag instead of whole character table entry! data integrity is crucial CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_REMOVE_GHOST); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); _SaveAuras(trans, false); @@ -6455,7 +6455,7 @@ void Player::ModifyHonorPoints(int32 value, CharacterDatabaseTransaction trans) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_HONOR_POINTS); stmt->SetData(0, newValue); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); trans->Append(stmt); } } @@ -6471,7 +6471,7 @@ void Player::ModifyArenaPoints(int32 value, CharacterDatabaseTransaction trans) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_ARENA_POINTS); stmt->SetData(0, newValue); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); trans->Append(stmt); } } @@ -9324,7 +9324,7 @@ void Player::RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent) // Handle removing pet while it is in "temporarily unsummoned" state, for example on mount CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_PET_SLOT_BY_ID); stmt->SetData(0, PET_SAVE_NOT_IN_SLOT); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); stmt->SetData(2, m_petStable->CurrentPet->PetNumber); CharacterDatabase.Execute(stmt); @@ -11502,7 +11502,7 @@ void Player::LeaveBattleground(Battleground* bg) if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_TRACK_DESERTERS)) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_DESERTER_TRACK); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, BG_DESERTION_TYPE_LEAVE_BG); CharacterDatabase.Execute(stmt); } @@ -14026,7 +14026,7 @@ void Player::_LoadSkills(PreparedQueryResult result) CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_SKILL); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, skill); CharacterDatabase.Execute(stmt); @@ -14971,7 +14971,7 @@ void Player::_SaveEquipmentSets(CharacterDatabaseTransaction trans) stmt->SetData(j++, eqset.IgnoreMask); for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i) stmt->SetData(j++, eqset.Items[i].GetCounter()); - stmt->SetData(j++, GetGUID().GetCounter()); + stmt->SetData(j++, GetGUID().GetRawValue()); stmt->SetData(j++, eqset.Guid); stmt->SetData(j, index); trans->Append(stmt); @@ -14980,7 +14980,7 @@ void Player::_SaveEquipmentSets(CharacterDatabaseTransaction trans) break; case EQUIPMENT_SET_NEW: stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_EQUIP_SET); - stmt->SetData(j++, GetGUID().GetCounter()); + stmt->SetData(j++, GetGUID().GetRawValue()); stmt->SetData(j++, eqset.Guid); stmt->SetData(j++, index); stmt->SetData(j++, eqset.Name.c_str()); @@ -15010,11 +15010,11 @@ void Player::_SaveEntryPoint(CharacterDatabaseTransaction trans) return; CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_ENTRY_POINT); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_PLAYER_ENTRY_POINT); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData (1, m_entryPointData.joinPos.GetPositionX()); stmt->SetData (2, m_entryPointData.joinPos.GetPositionY()); stmt->SetData (3, m_entryPointData.joinPos.GetPositionZ()); @@ -15050,7 +15050,7 @@ void Player::RemoveAtLoginFlag(AtLoginFlags flags, bool persist /*= false*/) CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_REM_AT_LOGIN_FLAG); stmt->SetData(0, uint16(flags)); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); CharacterDatabase.Execute(stmt); } @@ -15097,7 +15097,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) //! Insert query //! TO DO: Filter out more redundant fields that can take their default value at player create stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER); - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); stmt->SetData(index++, GetSession()->GetAccountId()); stmt->SetData(index++, GetName()); stmt->SetData(index++, getRace(true)); @@ -15355,7 +15355,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) stmt->SetData(index++, IsInWorld() && !GetSession()->PlayerLogout() ? 1 : 0); // Index - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); } trans->Append(stmt); @@ -15390,7 +15390,7 @@ void Player::_SaveGlyphs(CharacterDatabaseTransaction trans) return; CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GLYPHS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); for (uint8 spec = 0; spec < m_specsCount; ++spec) @@ -15398,7 +15398,7 @@ void Player::_SaveGlyphs(CharacterDatabaseTransaction trans) uint8 index = 0; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_GLYPHS); - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); stmt->SetData(index++, spec); for (uint8 i = 0; i < MAX_GLYPH_SLOT_INDEX; ++i) @@ -15445,7 +15445,7 @@ void Player::_SaveTalents(CharacterDatabaseTransaction trans) if (itr->second->State == PLAYERSPELL_REMOVED || itr->second->State == PLAYERSPELL_CHANGED) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TALENT_BY_SPELL); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); trans->Append(stmt); } @@ -15454,7 +15454,7 @@ void Player::_SaveTalents(CharacterDatabaseTransaction trans) if (itr->second->State == PLAYERSPELL_NEW || itr->second->State == PLAYERSPELL_CHANGED) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TALENT); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); stmt->SetData(2, itr->second->specMask); trans->Append(stmt); @@ -15627,7 +15627,7 @@ void Player::ActivateSpec(uint8 spec) // load them asynchronously { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, m_activeSpec); WorldSession* mySess = GetSession(); @@ -16142,7 +16142,7 @@ void Player::SetRandomWinner(bool isWinner) if (m_IsBGRandomWinner) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_BATTLEGROUND_RANDOM); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); CharacterDatabase.Execute(stmt); } } @@ -16259,7 +16259,7 @@ void Player::_LoadBrewOfTheMonth(PreparedQueryResult result) // Update Event Id CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_BREW_OF_THE_MONTH); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, uint32(eventId)); trans->Append(stmt); diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 8def7f56d..7083bfbd3 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -4978,7 +4978,7 @@ void Player::SetHomebind(WorldLocation const& loc, uint32 areaId) stmt->SetData (2, m_homebindX); stmt->SetData (3, m_homebindY); stmt->SetData (4, m_homebindZ); - stmt->SetData(5, GetGUID().GetCounter()); + stmt->SetData(5, GetGUID().GetRawValue()); CharacterDatabase.Execute(stmt); } @@ -5027,9 +5027,9 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons return false; } - ObjectGuid::LowType guid = playerGuid.GetCounter(); + uint64 guid = playerGuid.GetRawValue(); - Object::_Create(guid, 0, HighGuid::Player); + Object::_Create(playerGuid); m_name = fields[2].Get(); @@ -6101,7 +6101,7 @@ Item* Player::_LoadItem(CharacterDatabaseTransaction trans, uint32 zoneId, uint3 // xinef: sync query stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ITEM_REFUNDS); stmt->SetData(0, item->GetGUID().GetCounter()); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); if (PreparedQueryResult result = CharacterDatabase.Query(stmt)) { item->SetRefundRecipient((*result)[0].Get()); @@ -7152,7 +7152,7 @@ bool Player::_LoadHomeBind(PreparedQueryResult result) else { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_HOMEBIND); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); CharacterDatabase.Execute(stmt); } } @@ -7166,7 +7166,7 @@ bool Player::_LoadHomeBind(PreparedQueryResult result) m_homebindZ = info->positionZ; CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_PLAYER_HOMEBIND); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, m_homebindMapId); stmt->SetData(2, m_homebindAreaId); stmt->SetData (3, m_homebindX); @@ -7265,7 +7265,7 @@ void Player::SaveGoldToDB(CharacterDatabaseTransaction trans) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_MONEY); stmt->SetData(0, GetMoney()); - stmt->SetData(1, GetGUID().GetCounter()); + stmt->SetData(1, GetGUID().GetRawValue()); trans->Append(stmt); } @@ -7279,7 +7279,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans) { case ACTIONBUTTON_NEW: stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, m_activeSpec); stmt->SetData(2, itr->first); stmt->SetData(3, itr->second.GetAction()); @@ -7293,7 +7293,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans) stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ACTION); stmt->SetData(0, itr->second.GetAction()); stmt->SetData(1, uint8(itr->second.GetType())); - stmt->SetData(2, GetGUID().GetCounter()); + stmt->SetData(2, GetGUID().GetRawValue()); stmt->SetData(3, itr->first); stmt->SetData(4, m_activeSpec); trans->Append(stmt); @@ -7303,7 +7303,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans) break; case ACTIONBUTTON_DELETED: stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_BUTTON_SPEC); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); stmt->SetData(2, m_activeSpec); trans->Append(stmt); @@ -7320,7 +7320,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans) void Player::_SaveAuras(CharacterDatabaseTransaction trans, bool logout) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_AURA); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); for (AuraMap::const_iterator itr = m_ownedAuras.begin(); itr != m_ownedAuras.end(); ++itr) @@ -7355,7 +7355,7 @@ void Player::_SaveAuras(CharacterDatabaseTransaction trans, bool logout) uint8 index = 0; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_AURA); - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); stmt->SetData(index++, itr->second->GetCasterGUID().GetRawValue()); stmt->SetData(index++, itr->second->GetCastItemGUID().GetRawValue()); stmt->SetData(index++, itr->second->GetId()); @@ -7440,7 +7440,7 @@ void Player::_SaveInventory(CharacterDatabaseTransaction trans) if (m_itemUpdateQueue.empty()) return; - ObjectGuid::LowType lowGuid = GetGUID().GetCounter(); + uint64 guid = GetGUID().GetRawValue(); for (std::size_t i = 0; i < m_itemUpdateQueue.size(); ++i) { Item* item = m_itemUpdateQueue[i]; @@ -7459,12 +7459,12 @@ void Player::_SaveInventory(CharacterDatabaseTransaction trans) if (Item* test2 = GetItemByPos(INVENTORY_SLOT_BAG_0, item->GetBagSlot())) bagTestGUID = test2->GetGUID().GetCounter(); LOG_ERROR("entities.player", "Player(GUID: {} Name: {})::_SaveInventory - the bag({}) and slot({}) values for the item {} (state {}) are incorrect, the player doesn't have an item at that position!", - lowGuid, GetName(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString(), (int32)item->GetState()); + guid, GetName(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString(), (int32)item->GetState()); // according to the test that was just performed nothing should be in this slot, delete stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INVENTORY_BY_BAG_SLOT); stmt->SetData(0, bagTestGUID); stmt->SetData(1, item->GetSlot()); - stmt->SetData(2, lowGuid); + stmt->SetData(2, guid); trans->Append(stmt); RemoveTradeableItem(item); // pussywizard @@ -7480,7 +7480,7 @@ void Player::_SaveInventory(CharacterDatabaseTransaction trans) else if (test != item) { LOG_ERROR("entities.player", "Player(GUID: {} Name: {})::_SaveInventory - the bag({}) and slot({}) values for the item ({}) are incorrect, the item ({}) is there instead!", - lowGuid, GetName(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString(), test->GetGUID().ToString()); + guid, GetName(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString(), test->GetGUID().ToString()); // save all changes to the item... if (item->GetState() != ITEM_NEW) // only for existing items, no dupes item->SaveToDB(trans); @@ -7494,7 +7494,7 @@ void Player::_SaveInventory(CharacterDatabaseTransaction trans) case ITEM_NEW: case ITEM_CHANGED: stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_INVENTORY_ITEM); - stmt->SetData(0, lowGuid); + stmt->SetData(0, guid); stmt->SetData(1, bag_guid); stmt->SetData (2, item->GetSlot()); stmt->SetData(3, item->GetGUID().GetCounter()); @@ -7610,7 +7610,7 @@ void Player::_SaveQuestStatus(CharacterDatabaseTransaction trans) uint8 index = 0; stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_QUESTSTATUS); - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); stmt->SetData(index++, statusItr->first); stmt->SetData(index++, uint8(statusItr->second.Status)); stmt->SetData(index++, statusItr->second.Explored); @@ -7629,7 +7629,7 @@ void Player::_SaveQuestStatus(CharacterDatabaseTransaction trans) else { stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS_BY_QUEST); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, saveItr->first); trans->Append(stmt); } @@ -7644,7 +7644,7 @@ void Player::_SaveQuestStatus(CharacterDatabaseTransaction trans) else // xinef: what the is this? quest can be removed by spelleffect if (!keepAbandoned) stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS_REWARDED_BY_QUEST); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, saveItr->first); trans->Append(stmt); } @@ -7666,14 +7666,14 @@ void Player::_SaveDailyQuestStatus(CharacterDatabaseTransaction trans) // we don't need transactions here. CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_QUEST_STATUS_DAILY_CHAR); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) { if (GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1 + quest_daily_idx)) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_DAILYQUESTSTATUS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1 + quest_daily_idx)); stmt->SetData(2, uint64(m_lastDailyQuestTime)); trans->Append(stmt); @@ -7685,7 +7685,7 @@ void Player::_SaveDailyQuestStatus(CharacterDatabaseTransaction trans) for (DFQuestsDoneList::iterator itr = m_DFQuests.begin(); itr != m_DFQuests.end(); ++itr) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_DAILYQUESTSTATUS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, (*itr)); stmt->SetData(2, uint64(m_lastDailyQuestTime)); trans->Append(stmt); @@ -7700,7 +7700,7 @@ void Player::_SaveWeeklyQuestStatus(CharacterDatabaseTransaction trans) // we don't need transactions here. CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_QUEST_STATUS_WEEKLY_CHAR); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); for (QuestSet::const_iterator iter = m_weeklyquests.begin(); iter != m_weeklyquests.end(); ++iter) @@ -7708,7 +7708,7 @@ void Player::_SaveWeeklyQuestStatus(CharacterDatabaseTransaction trans) uint32 quest_id = *iter; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_WEEKLYQUESTSTATUS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, quest_id); trans->Append(stmt); } @@ -7725,7 +7725,7 @@ void Player::_SaveSeasonalQuestStatus(CharacterDatabaseTransaction trans) // we don't need transactions here. CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_QUEST_STATUS_SEASONAL_CHAR); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); m_SeasonalQuestChanged = false; @@ -7744,7 +7744,7 @@ void Player::_SaveSeasonalQuestStatus(CharacterDatabaseTransaction trans) uint32 questId = *itr; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_SEASONALQUESTSTATUS); - stmt->SetArguments(GetGUID().GetCounter(), questId, eventId); + stmt->SetArguments(GetGUID().GetRawValue(), questId, eventId); trans->Append(stmt); } } @@ -7757,14 +7757,14 @@ void Player::_SaveMonthlyQuestStatus(CharacterDatabaseTransaction trans) // we don't need transactions here. CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_QUEST_STATUS_MONTHLY_CHAR); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); for (QuestSet::const_iterator iter = m_monthlyquests.begin(); iter != m_monthlyquests.end(); ++iter) { uint32 quest_id = *iter; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_MONTHLYQUESTSTATUS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, quest_id); trans->Append(stmt); } @@ -7787,7 +7787,7 @@ void Player::_SaveSkills(CharacterDatabaseTransaction trans) if (itr->second.uState == SKILL_DELETED) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SKILL_BY_SKILL); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); trans->Append(stmt); @@ -7803,7 +7803,7 @@ void Player::_SaveSkills(CharacterDatabaseTransaction trans) { case SKILL_NEW: stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_SKILLS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, uint16(itr->first)); stmt->SetData(2, value); stmt->SetData(3, max); @@ -7814,7 +7814,7 @@ void Player::_SaveSkills(CharacterDatabaseTransaction trans) stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_SKILLS); stmt->SetData(0, value); stmt->SetData(1, max); - stmt->SetData(2, GetGUID().GetCounter()); + stmt->SetData(2, GetGUID().GetRawValue()); stmt->SetData(3, uint16(itr->first)); trans->Append(stmt); @@ -7845,7 +7845,7 @@ void Player::_SaveSpells(CharacterDatabaseTransaction trans) if (itr->second->State == PLAYERSPELL_REMOVED || itr->second->State == PLAYERSPELL_CHANGED) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SPELL_BY_SPELL); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); trans->Append(stmt); } @@ -7854,7 +7854,7 @@ void Player::_SaveSpells(CharacterDatabaseTransaction trans) if (itr->second->State == PLAYERSPELL_NEW || itr->second->State == PLAYERSPELL_CHANGED) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_SPELL); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); stmt->SetData(1, itr->first); stmt->SetData(2, itr->second->specMask); trans->Append(stmt); @@ -7884,13 +7884,13 @@ void Player::_SaveStats(CharacterDatabaseTransaction trans) CharacterDatabasePreparedStatement* stmt = nullptr; stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_STATS); - stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(0, GetGUID().GetRawValue()); trans->Append(stmt); uint8 index = 0; stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_STATS); - stmt->SetData(index++, GetGUID().GetCounter()); + stmt->SetData(index++, GetGUID().GetRawValue()); stmt->SetData(index++, GetMaxHealth()); for (uint8 i = 0; i < MAX_POWERS; ++i) diff --git a/src/server/game/Entities/Transport/Transport.cpp b/src/server/game/Entities/Transport/Transport.cpp index 0bbe0fae6..552fd4432 100644 --- a/src/server/game/Entities/Transport/Transport.cpp +++ b/src/server/game/Entities/Transport/Transport.cpp @@ -19,6 +19,7 @@ #include "Cell.h" #include "CellImpl.h" #include "Common.h" +#include "Config.h" #include "DBCStores.h" #include "GameObjectAI.h" #include "GameTime.h" @@ -30,8 +31,28 @@ #include "Spell.h" #include "Vehicle.h" #include "WorldModel.h" +#include -MotionTransport::MotionTransport() : Transport(), _transportInfo(nullptr), _isMoving(true), _pendingStop(false), _triggeredArrivalEvent(false), _triggeredDepartureEvent(false), _passengersLoaded(false), _delayedTeleport(false) +namespace +{ + // Any fixed reference epoch works; this one is 2023-07-12 05:20:00 UTC. + std::time_t const startTimestamp = 1689139200; + std::chrono::system_clock::time_point const transportStartDate = std::chrono::system_clock::from_time_t(startTimestamp); + + // Calculates time of the next departure cycle. + std::chrono::system_clock::time_point calculateNextDepartureTime(int oneIterationInterval) + { + std::chrono::system_clock::time_point currentTime = std::chrono::system_clock::now(); + std::chrono::milliseconds interval(oneIterationInterval); + std::chrono::milliseconds timeSinceStart = std::chrono::duration_cast(currentTime - transportStartDate); + int64 intervalsPassed = timeSinceStart.count() / oneIterationInterval; + std::chrono::system_clock::time_point nextDeparture = transportStartDate + (interval * (intervalsPassed + 1)); + return nextDeparture; + } +} + +MotionTransport::MotionTransport() : Transport(), _transportInfo(nullptr), _isMoving(true), _pendingStop(false), _triggeredArrivalEvent(false), _triggeredDepartureEvent(false), _passengersLoaded(false), _delayedTeleport(false), + _requiresFirstDepartureSync(false), _firstDepartureTime(transportStartDate) { m_updateFlag = UPDATEFLAG_TRANSPORT | UPDATEFLAG_LOWGUID | UPDATEFLAG_STATIONARY_POSITION | UPDATEFLAG_ROTATION; } @@ -76,6 +97,13 @@ bool MotionTransport::CreateMoTrans(ObjectGuid::LowType guidlow, uint32 entry, u _transportInfo = tInfo; + // Enable transport sync only in cluster mode and for transport with several maps. + if (sConfigMgr->GetOption("Cluster.Enabled", false) && tInfo->mapsUsed.size() > 1) + { + _requiresFirstDepartureSync = true; + this->SetPhaseMask(2, true); + } + // initialize waypoints _nextFrame = tInfo->keyFrames.begin(); _currentFrame = _nextFrame++; @@ -108,6 +136,56 @@ bool MotionTransport::CreateMoTrans(ObjectGuid::LowType guidlow, uint32 entry, u return true; } +// Delays first departure to make sure that transport follows strict schedule +// and with that makes transport synced between cluster nodes. +uint32 MotionTransport::HandleFirstDepartureSync(uint32 diff) +{ + if (!_requiresFirstDepartureSync) + return diff; + + if (_firstDepartureTime == transportStartDate) + { + _firstDepartureTime = calculateNextDepartureTime(_transportInfo->pathTime); + return diff; + } + + // Making system call for current time to be more accurate. + // Shouldn't be an issue since it runs only before the very first departure. + int32 millLeftToDeparture = std::chrono::duration_cast(_firstDepartureTime - std::chrono::system_clock::now()).count(); + if (millLeftToDeparture > 0) + return diff; + + // At this point we are ready for first departure. + _requiresFirstDepartureSync = false; + this->SetPhaseMask(1, true); + + // Players don't know about transport because it was in a different phase. + // We need to notify players that object exists before departure. + Map::PlayerList const& players = this->GetMap()->GetPlayers(); + if (!players.IsEmpty()) + { + for (Map::PlayerList::const_iterator i = players.begin(); i != players.end(); ++i) + { + if (Player* player = i->GetSource()) + { + // Same phase filter as Map::SendInitTransports: players outside + // the transport's phase must not get a create-block for it. + if (!player->InSamePhase(this)) + continue; + + UpdateData transData; + this->BuildCreateUpdateBlockForPlayer(&transData, player); + + WorldPacket packet; + transData.BuildPacket(packet); + player->SendDirectMessage(&packet); + } + } + } + + return millLeftToDeparture * -1; +} + void MotionTransport::CleanupsBeforeDelete(bool finalCleanup /*= true*/) { UnloadStaticPassengers(); @@ -137,6 +215,13 @@ void MotionTransport::BuildUpdate(UpdateDataMapType& data_map) void MotionTransport::Update(uint32 diff) { + if (_requiresFirstDepartureSync) + { + diff = HandleFirstDepartureSync(diff); + if (_requiresFirstDepartureSync) + return; + } + uint32 const positionUpdateDelay = 1; if (AI()) diff --git a/src/server/game/Entities/Transport/Transport.h b/src/server/game/Entities/Transport/Transport.h index 8287699df..dfdf094db 100644 --- a/src/server/game/Entities/Transport/Transport.h +++ b/src/server/game/Entities/Transport/Transport.h @@ -90,6 +90,8 @@ private: void UpdatePassengerPositions(PassengerSet& passengers); void DoEventIfAny(KeyFrame const& node, bool departure); + uint32 HandleFirstDepartureSync(uint32 diff); + //! Helpers to know if stop frame was reached bool IsMoving() const { return _isMoving; } void SetMoving(bool val) { _isMoving = val; } @@ -109,6 +111,9 @@ private: mutable std::mutex Lock; bool _passengersLoaded; bool _delayedTeleport; + + bool _requiresFirstDepartureSync; + std::chrono::system_clock::time_point _firstDepartureTime; }; class StaticTransport : public Transport diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index a194f57de..4da913a96 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -34,6 +34,7 @@ #include "Player.h" #include "ScriptMgr.h" #include "SharedDefines.h" +#include "TC9Sidecar.h" #include "UpdateFieldFlags.h" #include "Util.h" #include "World.h" @@ -329,7 +330,7 @@ void Group::ConvertToRaid() _initRaidSubGroupsCounter(); - if (!isBGGroup() && !isBFGroup()) + if (!sToCloud9Sidecar->ClusterModeEnabled() && !isBGGroup() && !isBFGroup()) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_GROUP_TYPE); @@ -577,6 +578,52 @@ bool Group::AddMember(Player* player, uint8 roles /* = 0 */) return true; } +void Group::AddMemberWithGuid(ObjectGuid guid) +{ + // Idempotent under sidecar event redelivery: never duplicate a MemberSlot. + if (IsMember(guid)) + return; + + if (Player* player = ObjectAccessor::FindPlayer(guid)) + { + AddMember(player); + return; + } + + // Get first not-full group + uint8 subGroup = 0; + if (m_subGroupsCounts) + { + bool groupFound = false; + for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup) + { + if (m_subGroupsCounts[subGroup] < MAXGROUPSIZE) + { + groupFound = true; + break; + } + } + // We are raid group and no one slot is free + if (!groupFound) + return; + } + + MemberSlot member; + member.guid = guid; + sCharacterCache->GetCharacterNameByGuid(guid, member.name); + member.group = subGroup; + member.flags = 0; + member.roles = 0; + m_memberSlots.push_back(member); + + if (!isBGGroup() && !isBFGroup()) + { + sCharacterCache->UpdateCharacterGroup(guid, GetGUID()); + } + + SubGroupCounterIncrease(subGroup); +} + bool Group::RemoveMember(ObjectGuid guid, RemoveMethod const& method /*= GROUP_REMOVEMETHOD_DEFAULT*/, ObjectGuid kicker /*= ObjectGuid::Empty*/, char const* reason /*= nullptr*/) { BroadcastGroupUpdate(); @@ -588,8 +635,14 @@ bool Group::RemoveMember(ObjectGuid guid, RemoveMethod const& method /*= GROUP_R return m_memberSlots.size() > 0; } - // remove member and change leader (if need) only if strong more 2 members _before_ member remove (BG/BF allow 1 member group) - if (GetMembersCount() > ((isBGGroup() || isLFGGroup() || isBFGroup()) ? 1u : 2u)) + // remove member and change leader (if need) only if strong more 2 members _before_ member remove (BG/BF allow 1 member group), + // except in cluster mode, where this branch is taken for any size: the group + // service owns the group lifecycle and local Disband() is a no-op, so always + // unlink the removed member here; waiting for the disband event leaves the + // last two members with a dangling group pointer whenever that event is lost + // (group service restart). + if (GetMembersCount() > ((isBGGroup() || isLFGGroup() || isBFGroup()) ? 1u : 2u) + || (sToCloud9Sidecar->ClusterModeEnabled() && !isBGGroup() && !isBFGroup())) { Player* player = ObjectAccessor::FindConnectedPlayer(guid); if (player) @@ -609,19 +662,24 @@ bool Group::RemoveMember(ObjectGuid guid, RemoveMethod const& method /*= GROUP_R player->UpdateForQuestWorldObjects(); } - WorldPacket data; - - if (method == GROUP_REMOVEMETHOD_KICK || method == GROUP_REMOVEMETHOD_KICK_LFG) + // BG/BF groups stay locally owned in cluster mode (see the gates above), + // so their removal packets must not be delegated to the group service. + if (!sToCloud9Sidecar->ClusterModeEnabled() || isBGGroup() || isBFGroup()) { - data.Initialize(SMSG_GROUP_UNINVITE, 0); - player->SendDirectMessage(&data); - } + WorldPacket data; - // Do we really need to send this opcode? - data.Initialize(SMSG_GROUP_LIST, 1 + 1 + 1 + 1 + 8 + 4 + 4 + 8); - data << uint8(0x10) << uint8(0) << uint8(0) << uint8(0); - data << m_guid << uint32(m_counter) << uint32(0) << uint64(0); - player->SendDirectMessage(&data); + if (method == GROUP_REMOVEMETHOD_KICK || method == GROUP_REMOVEMETHOD_KICK_LFG) + { + data.Initialize(SMSG_GROUP_UNINVITE, 0); + player->GetSession()->SendPacket(&data); + } + + // Do we really need to send this opcode? + data.Initialize(SMSG_GROUP_LIST, 1 + 1 + 1 + 1 + 8 + 4 + 4 + 8); + data << uint8(0x10) << uint8(0) << uint8(0) << uint8(0); + data << m_guid << uint32(m_counter) << uint32(0) << uint64(0); + player->GetSession()->SendPacket(&data); + } } // Remove player from group in DB @@ -756,7 +814,7 @@ void Group::ChangeLeader(ObjectGuid newLeaderGuid) sScriptMgr->OnGroupChangeLeader(this, newLeaderGuid, m_leaderGuid); // This hook should be executed at the end - Not used anywhere in the original core } -void Group::Disband(bool hideDestroy /* = false */) +void Group::ForcedDisband(bool hideDestroy /* = false */) { sScriptMgr->OnGroupDisband(this); @@ -852,6 +910,14 @@ void Group::Disband(bool hideDestroy /* = false */) delete this; } +void Group::Disband(bool hideDestroy /* = false */) +{ + if (sToCloud9Sidecar->ClusterModeEnabled() && !this->isBFGroup() && !this->isBGGroup()) + return; + + ForcedDisband(hideDestroy); +} + /*********************************************************/ /*** LOOT SYSTEM ***/ /*********************************************************/ @@ -1791,6 +1857,12 @@ void Group::SendTargetIconList(WorldSession* session) void Group::SendUpdate() { + if (sToCloud9Sidecar->ClusterModeEnabled() && !this->isBFGroup() && !this->isBGGroup()) + { + // Group service responsible for sending these updates. + return; + } + for (member_witerator witr = m_memberSlots.begin(); witr != m_memberSlots.end(); ++witr) SendUpdateToPlayer(witr->guid, &(*witr)); } @@ -2207,7 +2279,7 @@ void Roll::targetObjectBuildLink() void Group::SetDungeonDifficulty(Difficulty difficulty) { m_dungeonDifficulty = difficulty; - if (!isBGGroup() && !isBFGroup()) + if (!sToCloud9Sidecar->ClusterModeEnabled() && !isBGGroup() && !isBFGroup()) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_GROUP_DIFFICULTY); @@ -2228,7 +2300,7 @@ void Group::SetDungeonDifficulty(Difficulty difficulty) void Group::SetRaidDifficulty(Difficulty difficulty) { m_raidDifficulty = difficulty; - if (!isBGGroup() && !isBFGroup()) + if (!sToCloud9Sidecar->ClusterModeEnabled() && !isBGGroup() && !isBFGroup()) { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_GROUP_RAID_DIFFICULTY); diff --git a/src/server/game/Groups/Group.h b/src/server/game/Groups/Group.h index 50c51c451..32da01291 100644 --- a/src/server/game/Groups/Group.h +++ b/src/server/game/Groups/Group.h @@ -37,6 +37,7 @@ class Unit; class WorldObject; class WorldPacket; class WorldSession; +class ToCloud9GroupHooks; struct MapEntry; @@ -174,6 +175,7 @@ public: /** todo: uninvite people that not accepted invite **/ class Group { + friend class ToCloud9GroupHooks; public: struct MemberSlot { @@ -344,6 +346,9 @@ protected: void SubGroupCounterDecrease(uint8 subgroup); void ToggleGroupMemberFlag(member_witerator slot, uint8 flag, bool apply); + void AddMemberWithGuid(ObjectGuid guid); + void ForcedDisband(bool hideDestroy = false); + MemberSlotList m_memberSlots; GroupRefMgr m_memberMgr; InvitesList m_invitees; diff --git a/src/server/game/Groups/GroupMgr.cpp b/src/server/game/Groups/GroupMgr.cpp index 4ce847538..c4bf0e173 100644 --- a/src/server/game/Groups/GroupMgr.cpp +++ b/src/server/game/Groups/GroupMgr.cpp @@ -53,7 +53,11 @@ void GroupMgr::InitGroupIds() void GroupMgr::RegisterGroupId(ObjectGuid::LowType groupId) { - // Allocation was done in InitGroupIds() + // InitGroupIds() sizes the bitmap to the local MAX(guid) at startup. In cluster mode the + // group service assigns ids (auto-increment), which can exceed that bound, so grow on demand. + if (groupId >= _groupIds.size()) + _groupIds.resize(groupId + 1); + _groupIds[groupId] = true; // Groups are pulled in ascending order from db and _nextGroupId is initialized with 1, diff --git a/src/server/game/Handlers/ChannelHandler.cpp b/src/server/game/Handlers/ChannelHandler.cpp index 2193daaab..9319929b7 100644 --- a/src/server/game/Handlers/ChannelHandler.cpp +++ b/src/server/game/Handlers/ChannelHandler.cpp @@ -18,6 +18,8 @@ #include "ChannelMgr.h" #include "ObjectMgr.h" // for normalizePlayerName #include "Player.h" +#include "Language.h" +#include "TC9Sidecar.h" #include void WorldSession::HandleJoinChannel(WorldPacket& recvPacket) @@ -38,6 +40,23 @@ void WorldSession::HandleJoinChannel(WorldPacket& recvPacket) AreaTableEntry const* zone = sAreaTableStore.LookupEntry(GetPlayer()->GetZoneId()); if (!zone || !GetPlayer()->CanJoinConstantChannelInZone(channel, zone)) return; + + // Cluster mode rebuilds the localized channel name so nodes agree on it; + // stock servers keep the client-supplied name. + if (sToCloud9Sidecar->ClusterModeEnabled()) + { + auto const locale = GetSessionDbcLocale(); + std::string const& zoneName = zone->area_name[locale]; + std::string const cityName = sObjectMgr->GetAcoreStringForDBCLocale(LANG_CHANNEL_CITY); + char const* nameExt = (channel->flags & CHANNEL_DBC_FLAG_CITY_ONLY) ? cityName.c_str() : zoneName.c_str(); + + std::array buffer{}; + if (char const* pattern = channel->pattern[locale]) + { + std::snprintf(buffer.data(), buffer.size(), pattern, nameExt); + channelName = buffer.data(); + } + } } if (channelName.empty()) diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index 48ed07edc..ec41e893f 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -54,6 +54,7 @@ #include "SpellAuraEffects.h" #include "SpellAuras.h" #include "StringConvert.h" +#include "TC9Sidecar.h" #include "Tokenize.h" #include "Transport.h" #include "Util.h" @@ -81,125 +82,126 @@ bool LoginQueryHolder::Initialize() SetSize(MAX_PLAYER_LOGIN_QUERY); bool res = true; - ObjectGuid::LowType lowGuid = m_guid.GetCounter(); + uint64 rawGUID = m_guid.GetRawValue(); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_FROM, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_AURAS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_AURAS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SPELL); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SPELLS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_QUESTSTATUS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_DAILYQUESTSTATUS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_DAILY_QUEST_STATUS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_WEEKLYQUESTSTATUS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_WEEKLY_QUEST_STATUS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_MONTHLYQUESTSTATUS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MONTHLY_QUEST_STATUS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SEASONALQUESTSTATUS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SEASONAL_QUEST_STATUS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_REPUTATION); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_REPUTATION, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_INVENTORY); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_INVENTORY, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACTIONS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); + res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAILS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAILITEMS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SOCIALLIST); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SOCIAL_LIST, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_HOMEBIND); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_HOME_BIND, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SPELLCOOLDOWNS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SPELL_COOLDOWNS, stmt); if (sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED)) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_DECLINEDNAMES); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_DECLINED_NAMES, stmt); } stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACHIEVEMENTS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_CRITERIAPROGRESS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_EQUIPMENTSETS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_EQUIPMENT_SETS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ENTRY_POINT); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ENTRY_POINT, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_GLYPHS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_GLYPHS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_TALENTS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_TALENTS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_PLAYER_ACCOUNT_DATA); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACCOUNT_DATA, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SKILLS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SKILLS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_RANDOMBG); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RANDOM_BG, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_BANNED); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_BANNED, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_QUESTSTATUSREW); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS_REW, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_BREW_OF_THE_MONTH); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_BREW_OF_THE_MONTH, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES); @@ -207,19 +209,19 @@ bool LoginQueryHolder::Initialize() res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_INSTANCE_LOCK_TIMES, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CORPSE_LOCATION); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CORPSE_LOCATION, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_SETTINGS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CHARACTER_SETTINGS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PETS); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_PET_SLOTS, stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_ACHIEVEMENT_OFFLINE_UPDATES); - stmt->SetData(0, lowGuid); + stmt->SetData(0, rawGUID); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_OFFLINE_ACHIEVEMENTS_UPDATES, stmt); return res; @@ -699,7 +701,9 @@ void WorldSession::HandlePlayerLoginOpcode(WorldPacket& recvData) ObjectGuid playerGuid; recvData >> playerGuid; - if (!IsLegitCharacterForAccount(playerGuid)) + // The ownership check is delegated to the gateway in cluster mode, but a + // non-player GUID is invalid regardless of who authenticated the session. + if (!playerGuid.IsPlayer() || (!sToCloud9Sidecar->ClusterModeEnabled() && !IsLegitCharacterForAccount(playerGuid))) { LOG_ERROR("network", "Account ({}) can't login with that character ({}).", GetAccountId(), playerGuid.ToString()); KickPlayer("Account can't login with this character"); @@ -847,20 +851,28 @@ void WorldSession::HandlePlayerLoginFromDB(LoginQueryHolder const& holder) chH.PSendSysMessage("{}", GitRevision::GetFullVersion()); } - if (uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(pCurrChar->GetGUID())) + if (!sToCloud9Sidecar->ClusterModeEnabled()) { - Guild* guild = sGuildMgr->GetGuildById(guildId); - Guild::Member const* member = guild ? guild->GetMember(pCurrChar->GetGUID()) : nullptr; - if (member) + if (uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(pCurrChar->GetGUID())) { - pCurrChar->SetInGuild(guildId); - pCurrChar->SetRank(member->GetRankId()); - guild->SendLoginInfo(this); + Guild* guild = sGuildMgr->GetGuildById(guildId); + Guild::Member const* member = guild ? guild->GetMember(pCurrChar->GetGUID()) : nullptr; + if (member) + { + pCurrChar->SetInGuild(guildId); + pCurrChar->SetRank(member->GetRankId()); + guild->SendLoginInfo(this); + } + else + { + LOG_ERROR("network.opcode", "Player {} ({}) marked as member of not existing guild (id: {}), removing guild membership for player.", + pCurrChar->GetName(), pCurrChar->GetGUID().ToString(), guildId); + pCurrChar->SetInGuild(0); + pCurrChar->SetRank(0); + } } else { - LOG_ERROR("network.opcode", "Player {} ({}) marked as member of not existing guild (id: {}), removing guild membership for player.", - pCurrChar->GetName(), pCurrChar->GetGUID().ToString(), guildId); pCurrChar->SetInGuild(0); pCurrChar->SetRank(0); } @@ -914,9 +926,12 @@ void WorldSession::HandlePlayerLoginFromDB(LoginQueryHolder const& holder) pCurrChar->SendInitialPacketsAfterAddToMap(); - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ONLINE); - stmt->SetData(0, pCurrChar->GetGUID().GetCounter()); - CharacterDatabase.Execute(stmt); + if (!sToCloud9Sidecar->ClusterModeEnabled()) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ONLINE); + stmt->SetData(0, pCurrChar->GetGUID().GetCounter()); + CharacterDatabase.Execute(stmt); + } LoginDatabasePreparedStatement* loginStmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_ONLINE); loginStmt->SetData(0, realm.Id.Realm); diff --git a/src/server/game/Handlers/ChatHandler.cpp b/src/server/game/Handlers/ChatHandler.cpp index a9c9acb71..8adb05144 100644 --- a/src/server/game/Handlers/ChatHandler.cpp +++ b/src/server/game/Handlers/ChatHandler.cpp @@ -713,6 +713,18 @@ namespace Acore void WorldSession::HandleTextEmoteOpcode(WorldPacket& recvData) { + uint32 text_emote; + recvData >> text_emote; + + constexpr uint32 readyEmote = 126; + + // Handle confirmation redirect when the player types "/ready" after the new node becomes available for redirection. + if (text_emote == readyEmote && GetPlayer()->GetMap()->IsPlayerRedirectKickTimerActive()) + { + HandleTC9PrepareForRedirect(recvData); + return; + } + if (!GetPlayer()->IsAlive()) return; @@ -728,10 +740,9 @@ void WorldSession::HandleTextEmoteOpcode(WorldPacket& recvData) if (GetPlayer()->IsSpectator()) return; - uint32 text_emote, emoteNum; + uint32 emoteNum; ObjectGuid guid; - recvData >> text_emote; recvData >> emoteNum; recvData >> guid; diff --git a/src/server/game/Handlers/TradeHandler.cpp b/src/server/game/Handlers/TradeHandler.cpp index ef7590f65..991d6211c 100644 --- a/src/server/game/Handlers/TradeHandler.cpp +++ b/src/server/game/Handlers/TradeHandler.cpp @@ -27,6 +27,7 @@ #include "SocialMgr.h" #include "Spell.h" #include "SpellMgr.h" +#include "TC9Sidecar.h" #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" @@ -505,6 +506,79 @@ void WorldSession::HandleAcceptTradeOpcode(WorldPacket& /*recvPacket*/) } } + bool needsCrossrealmHandling = sToCloud9Sidecar->IsCrossrealm() && _player->GetGUID().GetRealmID() != trader->GetGUID().GetRealmID(); + + // Create new items for crossrealm usage. + if (needsCrossrealmHandling) + { + CharacterDatabasePreparedStatement* stmt = nullptr; + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + stmt = CharacterDatabase.GetPreparedStatement(CHAR_NO_OP_PROVIDE_REALM_CONTEXT); + stmt->SetData(0, _player->GetGUID().GetRealmID()); + trans->Append(stmt); + + for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; i++) + { + if (myItems[i]) + { + Item* newItem = myItems[i]->CloneItem(myItems[i]->GetCount(), trader); + if (!newItem) + { + ItemPosCountVec playerDst; + if (_player->CanStoreItem(NULL_BAG, NULL_SLOT, playerDst, myItems[i], false) == EQUIP_ERR_OK) + _player->MoveItemToInventory(playerDst, myItems[i], true, true); + + // Should we handle else statement here? + + continue; + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); + stmt->SetData(0, myItems[i]->GetGUID().GetCounter()); + trans->Append(stmt); + + delete myItems[i]; + + myItems[i] = newItem; + } + } + CharacterDatabase.CommitTransaction(trans); + + trans = CharacterDatabase.BeginTransaction(); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_NO_OP_PROVIDE_REALM_CONTEXT); + stmt->SetData(0, trader->GetGUID().GetRealmID()); + trans->Append(stmt); + + for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; i++) + { + if (hisItems[i]) + { + Item* newItem = hisItems[i]->CloneItem(hisItems[i]->GetCount(), _player); + if (!newItem) + { + ItemPosCountVec playerDst; + if (trader->CanStoreItem(NULL_BAG, NULL_SLOT, playerDst, hisItems[i], false) == EQUIP_ERR_OK) + trader->MoveItemToInventory(playerDst, hisItems[i], true, true); + + // Should we handle else statement here? + + continue; + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); + stmt->SetData(0, hisItems[i]->GetGUID().GetCounter()); + trans->Append(stmt); + + delete hisItems[i]; + + hisItems[i] = newItem; + } + } + CharacterDatabase.CommitTransaction(trans); + + } + // execute trade: 2. store moveItems(myItems, hisItems); @@ -575,11 +649,25 @@ void WorldSession::HandleAcceptTradeOpcode(WorldPacket& /*recvPacket*/) delete trader->m_trade; trader->m_trade = nullptr; - // desynchronized with the other saves here (SaveInventoryAndGoldToDB() not have own transaction guards) - CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); - _player->SaveInventoryAndGoldToDB(trans); - trader->SaveInventoryAndGoldToDB(trans); - CharacterDatabase.CommitTransaction(trans); + // We can't use single transaction with different databases. + if (needsCrossrealmHandling) + { + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + _player->SaveInventoryAndGoldToDB(trans); + CharacterDatabase.CommitTransaction(trans); + + trans = CharacterDatabase.BeginTransaction(); + trader->SaveInventoryAndGoldToDB(trans); + CharacterDatabase.CommitTransaction(trans); + } + else + { + // desynchronized with the other saves here (SaveInventoryAndGoldToDB() not have own transaction guards) + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + _player->SaveInventoryAndGoldToDB(trans); + trader->SaveInventoryAndGoldToDB(trans); + CharacterDatabase.CommitTransaction(trans); + } info.Status = TRADE_STATUS_TRADE_COMPLETE; trader->GetSession()->SendTradeStatus(info); diff --git a/src/server/game/Instances/InstanceSaveMgr.cpp b/src/server/game/Instances/InstanceSaveMgr.cpp index 76e0d2db0..d93f2fd72 100644 --- a/src/server/game/Instances/InstanceSaveMgr.cpp +++ b/src/server/game/Instances/InstanceSaveMgr.cpp @@ -29,6 +29,7 @@ #include "ObjectMgr.h" #include "Player.h" #include "ScriptMgr.h" +#include "TC9Sidecar.h" #include "Timer.h" #include "Transport.h" #include "World.h" @@ -164,6 +165,9 @@ InstanceSave::~InstanceSave() void InstanceSave::InsertToDB() { + if (sToCloud9Sidecar->ClusterModeEnabled() && !sToCloud9Sidecar->IsMapAssigned(GetMapId())) + return; + std::string data; uint32 completedEncounters = 0; @@ -454,6 +458,157 @@ void InstanceSaveMgr::LoadCharacterBinds() lock_instLists = false; } +// Runs on a std::async worker thread. Only the two synchronous CharacterDatabase +// reads happen here (safe: the synch connection pool is mutex-guarded per connection). +// All manager-state access is deferred to MergeWithNewInstanceSaves on the world thread. +InstanceMapLoadRows InstanceSaveMgr::LoadInstanceSavesAndBindsForMapIDs(std::vector const& mapIDs) +{ + InstanceMapLoadRows rows; + + std::stringstream mapIDsStr; + for (size_t i = 0; i < mapIDs.size(); ++i) + { + mapIDsStr << mapIDs[i]; + if (i < mapIDs.size() - 1) + mapIDsStr << ","; + } + + QueryResult result = CharacterDatabase.Query("SELECT id, map, resettime, difficulty, completedEncounters, data FROM instance WHERE map IN ({}) ORDER BY id ASC", mapIDsStr.str()); + if (result) + { + do + { + Field* fields = result->Fetch(); + + InstanceMapLoadRows::InstanceRow row; + row.instanceId = fields[0].Get(); + row.mapId = fields[1].Get(); + row.resetTime = time_t(fields[2].Get()); + row.difficulty = fields[3].Get(); + row.completedEncounters = fields[4].Get(); + row.data = fields[5].Get(); + rows.instances.push_back(std::move(row)); + } while (result->NextRow()); + } + + result = CharacterDatabase.Query("SELECT guid, instance, permanent, extended FROM character_instance WHERE instance IN (SELECT id FROM instance WHERE map IN ({}))", mapIDsStr.str()); + if (result) + { + do + { + Field* fields = result->Fetch(); + + InstanceMapLoadRows::BindRow row; + row.guidLow = fields[0].Get(); + row.instanceId = fields[1].Get(); + row.perm = fields[2].Get(); + row.extended = fields[3].Get(); + rows.binds.push_back(row); + } while (result->NextRow()); + } + + return rows; +} + +// Runs on the world thread (TC9 async completion callback). Safe to touch +// m_instanceSaveById, playerBindStorage, m_resetExtendedTimeByMapDifficulty and MapMgr here. +void InstanceSaveMgr::MergeWithNewInstanceSaves(InstanceMapLoadRows const& loadResult) +{ + // Same guard as LoadCharacterBinds: without it, an unbind cascading into + // DeleteInstanceSaveIfNeeded would delete the very DB rows being merged. + lock_instLists = true; + + for (InstanceMapLoadRows::InstanceRow const& row : loadResult.instances) + { + MapEntry const* entry = sMapStore.LookupEntry(row.mapId); + if (!entry) + { + LOG_ERROR("instance.save", "InstanceSaveMgr::MergeWithNewInstanceSaves: wrong mapid = {}, instanceid = {}!", row.mapId, row.instanceId); + continue; + } + + // Same row validation as AddInstanceSave for rows loaded on reassignment. + if (row.instanceId == 0) + { + LOG_ERROR("instance.save", "InstanceSaveMgr::MergeWithNewInstanceSaves: mapid = {}, wrong instanceid = {}!", row.mapId, row.instanceId); + continue; + } + + if (row.difficulty >= (entry->IsRaid() ? MAX_RAID_DIFFICULTY : MAX_DUNGEON_DIFFICULTY)) + { + LOG_ERROR("instance.save", "InstanceSaveMgr::MergeWithNewInstanceSaves: mapid = {}, instanceid = {}, wrong difficulty {}!", row.mapId, row.instanceId, row.difficulty); + continue; + } + + time_t extendedResetTime = 0; + if (entry->IsRaid() || row.difficulty > DUNGEON_DIFFICULTY_NORMAL) + extendedResetTime = GetExtendedResetTimeFor(row.mapId, Difficulty(row.difficulty)); + + InstanceSave* save = new InstanceSave(row.mapId, row.instanceId, Difficulty(row.difficulty), row.resetTime, extendedResetTime); + save->SetCompletedEncounterMask(row.completedEncounters); + save->SetInstanceData(row.data); + if (row.resetTime > 0) + save->SetResetTime(row.resetTime); + + InstanceSaveHashMap::iterator currentSave = m_instanceSaveById.find(row.instanceId); + if (currentSave != m_instanceSaveById.end()) + { + InstanceSave* oldSave = currentSave->second; + + // Unbind every player still pointing at the stale save before it is + // freed, in-memory only: the DB rows were just refetched and are + // rebound below. Iterate a copy, PlayerUnbindInstance mutates the list. + GuidList players = oldSave->m_playerList; + for (ObjectGuid const& playerGuid : players) + PlayerUnbindInstance(playerGuid, oldSave->GetMapId(), oldSave->GetDifficulty(), false); + + m_instanceSaveById.erase(currentSave); + delete oldSave; + } + m_instanceSaveById[row.instanceId] = save; + } + + for (InstanceMapLoadRows::BindRow const& row : loadResult.binds) + { + InstanceSaveHashMap::iterator itr = m_instanceSaveById.find(row.instanceId); + InstanceSave* save = itr != m_instanceSaveById.end() ? itr->second : nullptr; + if (!save) + continue; + + ObjectGuid guid = ObjectGuid::Create(row.guidLow); + + PlayerCreateBoundInstancesMaps(guid); + InstancePlayerBind& bind = playerBindStorage[guid]->m[save->GetDifficulty()][save->GetMapId()]; + if (bind.save) // pussywizard: another bind for the same map and difficulty! may happen because of mysql thread races + { + if (bind.perm) // already loaded perm -> delete currently checked one from db + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INSTANCE_BY_INSTANCE_GUID); + stmt->SetData(0, guid.GetCounter()); + stmt->SetData(1, row.instanceId); + CharacterDatabase.Execute(stmt); + continue; + } + else // override temp bind by newest one + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INSTANCE_BY_INSTANCE_GUID); + stmt->SetData(0, guid.GetCounter()); + stmt->SetData(1, bind.save->GetInstanceId()); + CharacterDatabase.Execute(stmt); + bind.save->RemovePlayer(guid, this); + } + } + bind.save = save; + bind.perm = row.perm; + bind.extended = row.extended; + save->AddPlayer(guid); + if (row.perm) + save->SetCanReset(false); + } + + lock_instLists = false; +} + void InstanceSaveMgr::ScheduleReset(time_t time, InstResetEvent event) { m_resetTimeQueue.insert(std::pair(time, event)); diff --git a/src/server/game/Instances/InstanceSaveMgr.h b/src/server/game/Instances/InstanceSaveMgr.h index ce9cd722a..341d3d18e 100644 --- a/src/server/game/Instances/InstanceSaveMgr.h +++ b/src/server/game/Instances/InstanceSaveMgr.h @@ -27,6 +27,7 @@ #include #include #include +#include struct InstanceTemplate; struct MapEntry; @@ -103,6 +104,31 @@ private: typedef std::unordered_map ResetTimeByMapDifficultyMap; +// Raw rows fetched on a worker thread by LoadInstanceSavesAndBindsForMapIDs. +// Contains no manager-owned state, so it is safe to build off the world thread +// and consume later on the world thread in MergeWithNewInstanceSaves. +struct InstanceMapLoadRows +{ + struct InstanceRow + { + uint32 instanceId; + uint16 mapId; + time_t resetTime; + uint8 difficulty; + uint32 completedEncounters; + std::string data; + }; + struct BindRow + { + uint32 guidLow; + uint32 instanceId; + bool perm; + bool extended; + }; + std::vector instances; + std::vector binds; +}; + class InstanceSaveMgr { friend class InstanceSave; @@ -133,6 +159,11 @@ public: void LoadInstanceSaves(); void LoadCharacterBinds(); + // Worker thread: performs only the (blocking) DB reads, touches no manager state. + [[nodiscard]] InstanceMapLoadRows LoadInstanceSavesAndBindsForMapIDs(std::vector const& mapIDs); + // World thread: builds InstanceSaves and player binds from the fetched rows and merges them in. + void MergeWithNewInstanceSaves(InstanceMapLoadRows const& loadResult); + [[nodiscard]] time_t GetResetTimeFor(uint32 mapid, Difficulty d) const { ResetTimeByMapDifficultyMap::const_iterator itr = m_resetTimeByMapDifficulty.find(MAKE_PAIR32(mapid, d)); diff --git a/src/server/game/Instances/InstanceScript.cpp b/src/server/game/Instances/InstanceScript.cpp index 14a5fac34..cf4d56aa0 100644 --- a/src/server/game/Instances/InstanceScript.cpp +++ b/src/server/game/Instances/InstanceScript.cpp @@ -31,6 +31,7 @@ #include "RBAC.h" #include "ScriptMgr.h" #include "Spell.h" +#include "TC9Sidecar.h" #include "WorldSession.h" BossBoundaryData::~BossBoundaryData() @@ -41,6 +42,9 @@ BossBoundaryData::~BossBoundaryData() void InstanceScript::SaveToDB() { + if (sToCloud9Sidecar->ClusterModeEnabled() && !sToCloud9Sidecar->IsMapAssigned(instance->GetEntry()->MapID)) + return; + std::string data = GetSaveData(); //if (data.empty()) // pussywizard: encounterMask can be updated and theres no reason to not save // return; diff --git a/src/server/game/Maps/Map.cpp b/src/server/game/Maps/Map.cpp index cc045f67f..113215aa8 100644 --- a/src/server/game/Maps/Map.cpp +++ b/src/server/game/Maps/Map.cpp @@ -38,6 +38,7 @@ #include "Pet.h" #include "PoolMgr.h" #include "ScriptMgr.h" +#include "TC9Sidecar.h" #include "Transport.h" #include "VMapFactory.h" #include "Vehicle.h" @@ -518,6 +519,8 @@ void Map::Update(const uint32 t_diff, const uint32 s_diff, bool /*thread*/) HandleDelayedVisibility(); + UpdatePlayersRedirectKickEvent(t_diff); + UpdateWeather(t_diff); UpdateExpiredCorpses(t_diff); @@ -1670,7 +1673,7 @@ void Map::SendInitTransports(Player* player) // Hack to send out transports UpdateData transData; for (TransportsContainer::const_iterator itr = _transports.begin(); itr != _transports.end(); ++itr) - if (*itr != player->GetTransport()) + if (*itr != player->GetTransport() && (!sToCloud9Sidecar->ClusterModeEnabled() || player->InSamePhase(*itr))) (*itr)->BuildCreateUpdateBlockForPlayer(&transData, player); if (!transData.HasData()) @@ -1848,6 +1851,50 @@ uint32 Map::GetPlayersCountExceptGMs(bool aliveOnly /*= false*/) const return count; } +void Map::StartPlayersRedirectKickTimer() +{ + for (MapRefMgr::iterator itr = m_mapRefMgr.begin(); itr != m_mapRefMgr.end(); ++itr) + itr->GetSource()->SendSystemMessage("Preparing to enter parallel dimension... One minute!\nAccelerate transfer: Teleport or type \"/ready\" in chat."); + + _redirectKickTimer.Reset(60 * SECOND * IN_MILLISECONDS); + _lastAnnounceRedirectKickTimer.Reset(55 * SECOND * IN_MILLISECONDS); + + _lastAnnounceRedirectKickTimer.Update(1); + +} + +void Map::StopPlayersRedirectKickTimer() +{ + _redirectKickTimer.Reset(0); + _lastAnnounceRedirectKickTimer.Reset(0); +} + +void Map::UpdatePlayersRedirectKickEvent(uint32 diff) +{ + if (_redirectKickTimer.Passed()) + return; + + _redirectKickTimer.Update(diff); + + if (_redirectKickTimer.Passed()) + { + auto emptyPacket = WorldPacket(); + for (MapRefMgr::iterator itr = m_mapRefMgr.begin(); itr != m_mapRefMgr.end(); ++itr) + itr->GetSource()->GetSession()->HandleTC9PrepareForRedirect(emptyPacket); + + return; + } + + if (_lastAnnounceRedirectKickTimer.Passed()) + return; + + _lastAnnounceRedirectKickTimer.Update(diff); + + if (_lastAnnounceRedirectKickTimer.Passed()) + for (MapRefMgr::iterator itr = m_mapRefMgr.begin(); itr != m_mapRefMgr.end(); ++itr) + itr->GetSource()->SendSystemMessage("Dimensional shift incoming! Prepare to transition in 5 seconds..."); +} + void Map::SendToPlayers(WorldPacket const* data) const { for (MapRefMgr::const_iterator itr = m_mapRefMgr.begin(); itr != m_mapRefMgr.end(); ++itr) diff --git a/src/server/game/Maps/Map.h b/src/server/game/Maps/Map.h index 3575b91ff..09342e6fa 100644 --- a/src/server/game/Maps/Map.h +++ b/src/server/game/Maps/Map.h @@ -27,6 +27,7 @@ #include "GameObjectModel.h" #include "GridDefines.h" #include "GridRefMgr.h" +#include "Timer.h" #include "MapCollisionData.h" #include "MapGridManager.h" #include "MapRefMgr.h" @@ -325,6 +326,10 @@ public: void SendToPlayers(WorldPacket const* data) const; + void StartPlayersRedirectKickTimer(); + void StopPlayersRedirectKickTimer(); + bool IsPlayerRedirectKickTimerActive() { return !_redirectKickTimer.Passed(); } + typedef MapRefMgr PlayerList; [[nodiscard]] PlayerList const& GetPlayers() const { return m_mapRefMgr; } @@ -585,6 +590,8 @@ private: void SendObjectUpdates(); + void UpdatePlayersRedirectKickEvent(uint32 diff); + protected: // Type specific code for add/remove to/from grid template @@ -693,6 +700,9 @@ private: PendingAddUpdatableObjectList _pendingAddUpdatableObjectList; IntervalTimer _updatableObjectListRecheckTimer; ZoneWideVisibleWorldObjectsMap _zoneWideVisibleWorldObjectsMap; + + TimeTrackerSmall _redirectKickTimer; + TimeTrackerSmall _lastAnnounceRedirectKickTimer; }; enum InstanceResetMethod diff --git a/src/server/game/Maps/MapMgr.cpp b/src/server/game/Maps/MapMgr.cpp index 94044f799..46f627a10 100644 --- a/src/server/game/Maps/MapMgr.cpp +++ b/src/server/game/Maps/MapMgr.cpp @@ -31,6 +31,7 @@ #include "Opcodes.h" #include "Player.h" #include "ScriptMgr.h" +#include "TC9Sidecar.h" #include "Transport.h" #include "World.h" #include "WorldPacket.h" @@ -405,6 +406,9 @@ void MapMgr::RegisterInstanceId(uint32 instanceId) uint32 MapMgr::GenerateInstanceId() { + if (sToCloud9Sidecar->ClusterModeEnabled()) + return sToCloud9Sidecar->GenerateInstanceGuid(); + uint32 newInstanceId = _nextInstanceId; // find the lowest available id starting from the current _nextInstanceId diff --git a/src/server/game/Maps/TransportMgr.cpp b/src/server/game/Maps/TransportMgr.cpp index 00afdf2b6..ca5ef2d8b 100644 --- a/src/server/game/Maps/TransportMgr.cpp +++ b/src/server/game/Maps/TransportMgr.cpp @@ -21,6 +21,9 @@ #include "MoveSpline.h" #include "QueryResult.h" #include "Transport.h" +#include "TaskScheduler.h" +#include "Config.h" +#include TransportTemplate::~TransportTemplate() { diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp index 55113a806..c2d99161e 100644 --- a/src/server/game/Server/Protocol/Opcodes.cpp +++ b/src/server/game/Server/Protocol/Opcodes.cpp @@ -1439,6 +1439,8 @@ void OpcodeTable::Initialize() /*0x51C*/ DEFINE_SERVER_OPCODE_HANDLER(SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT1, STATUS_NEVER); /*0x51D*/ DEFINE_SERVER_OPCODE_HANDLER(SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT2, STATUS_NEVER); /*0x51E*/ DEFINE_SERVER_OPCODE_HANDLER(SMSG_MULTIPLE_MOVES, STATUS_NEVER); + /*0x51F*/ DEFINE_HANDLER(TC9_CMSG_PREPARE_FOR_REDIRECT, STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleTC9PrepareForRedirect); + /*0x520*/ DEFINE_SERVER_OPCODE_HANDLER(TC9_SMSG_READY_FOR_REDIRECT, STATUS_NEVER); #undef DEFINE_HANDLER #undef DEFINE_SERVER_OPCODE_HANDLER diff --git a/src/server/game/Server/Protocol/Opcodes.h b/src/server/game/Server/Protocol/Opcodes.h index cb994b3e8..0b34344c4 100644 --- a/src/server/game/Server/Protocol/Opcodes.h +++ b/src/server/game/Server/Protocol/Opcodes.h @@ -1338,7 +1338,9 @@ enum Opcodes : uint16 SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT1 = 0x51C, SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT2 = 0x51D, SMSG_MULTIPLE_MOVES = 0x51E, // uncompressed version of SMSG_COMPRESSED_MOVES - NUM_MSG_TYPES = 0x51F + TC9_CMSG_PREPARE_FOR_REDIRECT = 0x51F, + TC9_SMSG_READY_FOR_REDIRECT = 0x520, + NUM_MSG_TYPES = 0x521 }; enum OpcodeMisc : uint16 diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index b144d483f..3982998d1 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -19,6 +19,7 @@ \ingroup u2w */ +#include "TC9Sidecar.h" #include "WorldSession.h" #include "AccountMgr.h" #include "BattlegroundMgr.h" @@ -673,7 +674,7 @@ void WorldSession::SendPlayTimeWarning(PlayTimeFlag flag, int32 playTimeRemainin } /// %Log the player out -void WorldSession::LogoutPlayer(bool save) +void WorldSession::LogoutPlayer(bool save, bool redirecting) { // finish pending transfers before starting the logout while (_player && _player->IsBeingTeleportedFar()) @@ -759,23 +760,26 @@ void WorldSession::LogoutPlayer(bool save) // there are some positive auras from boss encounters that can be kept by logging out and logging in after boss is dead, and may be used on next bosses _player->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_CHANGE_MAP); - if (Group *group = _player->GetGroupInvite()) - sWorld->getBoolConfig(CONFIG_LEAVE_GROUP_ON_LOGOUT) - ? _player->UninviteFromGroup() // Can disband group. - : group->RemoveInvite(_player); // Just removes invite. + if (!redirecting) + { + if (Group *group = _player->GetGroupInvite()) + sWorld->getBoolConfig(CONFIG_LEAVE_GROUP_ON_LOGOUT) + ? _player->UninviteFromGroup() // Can disband group. + : group->RemoveInvite(_player); // Just removes invite. - // remove player from the group if he is: - // a) in group; b) not in raid group; c) logging out normally (not being kicked or disconnected) d) LeaveGroupOnLogout is enabled - if (_player->GetGroup() && !_player->GetGroup()->isRaidGroup() && !_player->GetGroup()->isLFGGroup() && m_Socket && sWorld->getBoolConfig(CONFIG_LEAVE_GROUP_ON_LOGOUT)) - _player->RemoveFromGroup(); - // Remove player from active loot rolls in LFG groups (player stays in group but should not block rolls) - else if (Group* group = _player->GetGroup()) - if (group->isLFGGroup()) - group->RemovePlayerFromRolls(_player->GetGUID()); + // remove player from the group if he is: + // a) in group; b) not in raid group; c) logging out normally (not being kicked or disconnected) d) LeaveGroupOnLogout is enabled + if (!sToCloud9Sidecar->ClusterModeEnabled() && _player->GetGroup() && !_player->GetGroup()->isRaidGroup() && !_player->GetGroup()->isLFGGroup() && m_Socket && sWorld->getBoolConfig(CONFIG_LEAVE_GROUP_ON_LOGOUT)) + _player->RemoveFromGroup(); + // Remove player from active loot rolls in LFG groups (player stays in group but should not block rolls) + else if (Group* group = _player->GetGroup()) + if (group->isLFGGroup()) + group->RemovePlayerFromRolls(_player->GetGUID()); - // pussywizard: checked second time after being removed from a group - if (!_player->IsBeingTeleportedFar() && !_player->m_InstanceValid && !_player->IsGameMaster()) - _player->RepopAtGraveyard(); + // pussywizard: checked second time after being removed from a group + if (!_player->IsBeingTeleportedFar() && !_player->m_InstanceValid && !_player->IsGameMaster()) + _player->RepopAtGraveyard(); + } // Repop at Graveyard or other player far teleport will prevent saving player because of not present map // Teleport player immediately for correct player save @@ -814,12 +818,15 @@ void WorldSession::LogoutPlayer(bool save) } } - //! Broadcast a logout message to the player's friends - sSocialMgr->SendFriendStatus(_player, FRIEND_OFFLINE, _player->GetGUID(), true); - sSocialMgr->RemovePlayerSocial(_player->GetGUID()); + if (!redirecting) + { + //! Broadcast a logout message to the player's friends + sSocialMgr->SendFriendStatus(_player, FRIEND_OFFLINE, _player->GetGUID(), true); + sSocialMgr->RemovePlayerSocial(_player->GetGUID()); - //! Call script hook before deletion - sScriptMgr->OnPlayerLogout(_player); + //! Call script hook before deletion + sScriptMgr->OnPlayerLogout(_player); + } METRIC_EVENT("player_events", "Logout", _player->GetName()); @@ -845,9 +852,12 @@ void WorldSession::LogoutPlayer(bool save) LOG_DEBUG("network", "SESSION: Sent SMSG_LOGOUT_COMPLETE Message"); //! Since each account can only have one online character at any given time, ensure all characters for active account are marked as offline - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ACCOUNT_ONLINE); - stmt->SetData(0, GetAccountId()); - CharacterDatabase.Execute(stmt); + if (!redirecting) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ACCOUNT_ONLINE); + stmt->SetData(0, GetAccountId()); + CharacterDatabase.Execute(stmt); + } } m_playerLogout = false; @@ -1563,21 +1573,69 @@ void WorldSession::InitializeSessionCallback(CharacterDatabaseQueryHolder const& LoadAccountData(realmHolder.GetPreparedResult(AccountInfoQueryHolderPerRealm::GLOBAL_ACCOUNT_DATA), GLOBAL_CACHE_MASK); LoadTutorialsData(realmHolder.GetPreparedResult(AccountInfoQueryHolderPerRealm::TUTORIALS)); - if (!m_inQueue) + if (!sToCloud9Sidecar->ClusterModeEnabled()) { - SendAuthResponse(AUTH_OK, true); - } - else - { - SendAuthWaitQueue(0); + if (!m_inQueue) + { + SendAuthResponse(AUTH_OK, true); + } + else + { + SendAuthWaitQueue(0); + } } SetInQueue(false); ResetTimeOutTime(false); - SendAddonsInfo(); - SendClientCacheVersion(clientCacheVersion); - SendTutorialsData(); + if (!sToCloud9Sidecar->ClusterModeEnabled()) + { + SendAddonsInfo(); + SendClientCacheVersion(clientCacheVersion); + SendTutorialsData(); + } +} + +void WorldSession::HandleTC9PrepareForRedirect(WorldPacket& /*recvData*/) +{ + if (!sToCloud9Sidecar->ClusterModeEnabled()) + return; + + Player* player = this->GetPlayer(); + if (player == nullptr) + { + WorldPacket data(TC9_SMSG_READY_FOR_REDIRECT, 1); + data << uint8(1); // 1 - Failed. + SendPacket(&data); + return; + } + + LOG_DEBUG("network", "Starting saving, AccountId = {}", GetAccountId()); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + player->SaveToDB(trans, false, true); + AddTransactionCallback(CharacterDatabase.AsyncCommitTransaction(trans)).AfterComplete([this](bool success) + { + WorldPacket data(TC9_SMSG_READY_FOR_REDIRECT, 1); + data << uint8(!success); // 0 - Success, 1 - Failed. + SendPacket(&data); + + if (!success) + { + LOG_ERROR("network", "Failed to save player, AccountId = {}", GetAccountId()); + return; + } + + LOG_DEBUG("network", "Saved, AccountId = {}", GetAccountId()); + + Player* player = GetPlayer(); + if (!player) + return; + + player->m_Events.AddEventAtOffset([this](){ + KickPlayer("HandlePrepareForRedirect client redirected"); + }, 100ms); + }); } void WorldSession::SetPacketLogging(bool state) diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index 8e6ec9be5..0abedb40c 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -524,7 +524,7 @@ public: return (_logoutTime > 0 && currTime >= _logoutTime + 20); } - void LogoutPlayer(bool save); + void LogoutPlayer(bool save, bool redirecting = false); void KickPlayer(bool setKicked = true) { return this->KickPlayer("Unknown reason", setKicked); } void KickPlayer(std::string const& reason, bool setKicked = true); @@ -687,6 +687,8 @@ public: // opcodes handlers void SendCharFactionChange(ResponseCodes result, CharacterFactionChangeInfo const* factionChangeInfo); void SendSetPlayerDeclinedNamesResult(DeclinedNameResult result, ObjectGuid guid); + void HandleTC9PrepareForRedirect(WorldPacket& recvData); + // played time void HandlePlayedTime(WorldPackets::Character::PlayedTimeClient& packet); diff --git a/src/server/game/Server/WorldSocket.cpp b/src/server/game/Server/WorldSocket.cpp index e4ca47d10..9553b8bee 100644 --- a/src/server/game/Server/WorldSocket.cpp +++ b/src/server/game/Server/WorldSocket.cpp @@ -15,6 +15,7 @@ * with this program. If not, see . */ +#include "TC9Sidecar.h" #include "WorldSocket.h" #include "AccountMgr.h" #include "Config.h" @@ -579,8 +580,9 @@ void WorldSocket::HandleAuthSessionCallback(std::shared_ptr a LoginDatabase.Execute(stmt); // This also allows to check for possible "hack" attempts on account - // even if auth credentials are bad, try using the session key we have - client cannot read auth response error without it - _authCrypt.Init(account.SessionKey); + if (!sToCloud9Sidecar->ClusterModeEnabled()) + // even if auth credentials are bad, try using the session key we have - client cannot read auth response error without it + _authCrypt.Init(account.SessionKey); // First reject the connection if packet contains invalid data or realm state doesn't allow logging in if (sWorld->IsClosed()) @@ -591,7 +593,7 @@ void WorldSocket::HandleAuthSessionCallback(std::shared_ptr a return; } - if (authSession->RealmID != realm.Id.Realm) + if (!sToCloud9Sidecar->ClusterModeEnabled() && authSession->RealmID != realm.Id.Realm) { SendAuthResponseError(REALM_LIST_REALM_NOT_FOUND); LOG_ERROR("network", "WorldSocket::HandleAuthSession: Client {} requested connecting with realm id {} but this realm has id {} set in config.", @@ -600,105 +602,107 @@ void WorldSocket::HandleAuthSessionCallback(std::shared_ptr a return; } - // Must be done before WorldSession is created bool wardenActive = sWorld->getBoolConfig(CONFIG_WARDEN_ENABLED); - if (wardenActive && account.OS != "Win" && account.OS != "OSX") + if (!sToCloud9Sidecar->ClusterModeEnabled()) { - SendAuthResponseError(AUTH_REJECT); - LOG_ERROR("network", "WorldSocket::HandleAuthSession: Client {} attempted to log in using invalid client OS ({}).", address, account.OS); - DelayedCloseSocket(); - return; - } + // Must be done before WorldSession is created + if (wardenActive && account.OS != "Win" && account.OS != "OSX") + { + SendAuthResponseError(AUTH_REJECT); + LOG_ERROR("network", "WorldSocket::HandleAuthSession: Client {} attempted to log in using invalid client OS ({}).", address, account.OS); + DelayedCloseSocket(); + return; + } - // Check that Key and account name are the same on client and server - uint8 t[4] = { 0x00,0x00,0x00,0x00 }; + // Check that Key and account name are the same on client and server + uint8 t[4] = { 0x00,0x00,0x00,0x00 }; - Acore::Crypto::SHA1 sha; - sha.UpdateData(authSession->Account); - sha.UpdateData(t); - sha.UpdateData(authSession->LocalChallenge); - sha.UpdateData(_authSeed); - sha.UpdateData(account.SessionKey); - sha.Finalize(); + Acore::Crypto::SHA1 sha; + sha.UpdateData(authSession->Account); + sha.UpdateData(t); + sha.UpdateData(authSession->LocalChallenge); + sha.UpdateData(_authSeed); + sha.UpdateData(account.SessionKey); + sha.Finalize(); - if (sha.GetDigest() != authSession->Digest) - { - SendAuthResponseError(AUTH_FAILED); - LOG_ERROR("network", "WorldSocket::HandleAuthSession: Authentication failed for account: {} ('{}') address: {}", account.Id, authSession->Account, address); - DelayedCloseSocket(); - return; - } - - if (IpLocationRecord const* location = sIPLocation->GetLocationRecord(address)) - _ipCountry = location->CountryCode; - - ///- Re-check ip locking (same check as in auth). - if (account.IsLockedToIP) - { - if (account.LastIP != address) + if (sha.GetDigest() != authSession->Digest) { SendAuthResponseError(AUTH_FAILED); - LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account IP differs. Original IP: {}, new IP: {}).", account.LastIP, address); - // We could log on hook only instead of an additional db log, however action logger is config based. Better keep DB logging as well + LOG_ERROR("network", "WorldSocket::HandleAuthSession: Authentication failed for account: {} ('{}') address: {}", account.Id, authSession->Account, address); + DelayedCloseSocket(); + return; + } + + if (IpLocationRecord const* location = sIPLocation->GetLocationRecord(address)) + _ipCountry = location->CountryCode; + + ///- Re-check ip locking (same check as in auth). + if (account.IsLockedToIP) + { + if (account.LastIP != address) + { + SendAuthResponseError(AUTH_FAILED); + LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account IP differs. Original IP: {}, new IP: {}).", account.LastIP, address); + // We could log on hook only instead of an additional db log, however action logger is config based. Better keep DB logging as well + sScriptMgr->OnFailedAccountLogin(account.Id); + DelayedCloseSocket(); + return; + } + } + else if (!account.LockCountry.empty() && account.LockCountry != "00" && !_ipCountry.empty()) + { + if (account.LockCountry != _ipCountry) + { + SendAuthResponseError(AUTH_FAILED); + LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account country differs. Original country: {}, new country: {}).", account.LockCountry, _ipCountry); + // We could log on hook only instead of an additional db log, however action logger is config based. Better keep DB logging as well + sScriptMgr->OnFailedAccountLogin(account.Id); + DelayedCloseSocket(); + return; + } + } + + //! Negative mutetime indicates amount of minutes to be muted effective on next login - which is now. + if (account.MuteTime < 0) + { + account.MuteTime = GameTime::GetGameTime().count() + std::llabs(account.MuteTime); + + auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_MUTE_TIME_LOGIN); + stmt->SetData(0, account.MuteTime); + stmt->SetData(1, account.Id); + LoginDatabase.Execute(stmt); + } + + if (account.IsBanned) + { + SendAuthResponseError(AUTH_BANNED); + LOG_ERROR("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account banned)."); sScriptMgr->OnFailedAccountLogin(account.Id); DelayedCloseSocket(); return; } - } - else if (!account.LockCountry.empty() && account.LockCountry != "00" && !_ipCountry.empty()) - { - if (account.LockCountry != _ipCountry) + + // Check locked state for server + AccountTypes allowedAccountType = sWorld->GetPlayerSecurityLimit(); + LOG_DEBUG("network", "Allowed Level: {} Player Level {}", allowedAccountType, account.Security); + if (allowedAccountType > SEC_PLAYER && account.Security < allowedAccountType) { - SendAuthResponseError(AUTH_FAILED); - LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account country differs. Original country: {}, new country: {}).", account.LockCountry, _ipCountry); - // We could log on hook only instead of an additional db log, however action logger is config based. Better keep DB logging as well + SendAuthResponseError(AUTH_UNAVAILABLE); + LOG_DEBUG("network", "WorldSocket::HandleAuthSession: User tries to login but his security level is not enough"); sScriptMgr->OnFailedAccountLogin(account.Id); DelayedCloseSocket(); return; } - } - //! Negative mutetime indicates amount of minutes to be muted effective on next login - which is now. - if (account.MuteTime < 0) - { - account.MuteTime = GameTime::GetGameTime().count() + std::llabs(account.MuteTime); + LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Client '{}' authenticated successfully from {}.", authSession->Account, address); - auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_MUTE_TIME_LOGIN); - stmt->SetData(0, account.MuteTime); - stmt->SetData(1, account.Id); + // Update the last_ip in the database as it was successful for login + stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LAST_IP); + stmt->SetData(0, address); + stmt->SetData(1, authSession->Account); LoginDatabase.Execute(stmt); } - if (account.IsBanned) - { - SendAuthResponseError(AUTH_BANNED); - LOG_ERROR("network", "WorldSocket::HandleAuthSession: Sent Auth Response (Account banned)."); - sScriptMgr->OnFailedAccountLogin(account.Id); - DelayedCloseSocket(); - return; - } - - // Check locked state for server - AccountTypes allowedAccountType = sWorld->GetPlayerSecurityLimit(); - LOG_DEBUG("network", "Allowed Level: {} Player Level {}", allowedAccountType, account.Security); - if (allowedAccountType > SEC_PLAYER && account.Security < allowedAccountType) - { - SendAuthResponseError(AUTH_UNAVAILABLE); - LOG_DEBUG("network", "WorldSocket::HandleAuthSession: User tries to login but his security level is not enough"); - sScriptMgr->OnFailedAccountLogin(account.Id); - DelayedCloseSocket(); - return; - } - - LOG_DEBUG("network", "WorldSocket::HandleAuthSession: Client '{}' authenticated successfully from {}.", authSession->Account, address); - - // Update the last_ip in the database as it was successful for login - stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LAST_IP); - stmt->SetData(0, address); - stmt->SetData(1, authSession->Account); - - LoginDatabase.Execute(stmt); - // At this point, we can safely hook a successful login sScriptMgr->OnAccountLogin(account.Id); @@ -712,8 +716,9 @@ void WorldSocket::HandleAuthSessionCallback(std::shared_ptr a _worldSession->ReadAddonsInfo(authSession->AddonInfo); // Initialize Warden system only if it is enabled by config - if (wardenActive) + if (!sToCloud9Sidecar->ClusterModeEnabled() && wardenActive) { + // TODO: move warden outside of a node? _worldSession->InitWarden(account.SessionKey, account.OS); } diff --git a/src/server/game/TC9Sidecar/AsyncTask.h b/src/server/game/TC9Sidecar/AsyncTask.h new file mode 100644 index 000000000..1736e1548 --- /dev/null +++ b/src/server/game/TC9Sidecar/AsyncTask.h @@ -0,0 +1,93 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _ASYNC_TASK_H +#define _ASYNC_TASK_H + +#include "Errors.h" +#include "Log.h" +#include +#include +#include + +template +class AsyncTask +{ +public: + using AsyncFunction = std::function; + using CallbackFunction = std::function; + + AsyncTask(AsyncFunction asyncFunc, CallbackFunction callbackFunc) + : asyncFunc(std::move(asyncFunc)), callbackFunc(std::move(callbackFunc)), isReady(false) + { + } + + ~AsyncTask() + { + // Ensure that the asynchronous task has completed before destruction + if (asyncTask.valid() && asyncTask.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + { + asyncTask.wait(); // Wait for the task to complete + } + } + + bool InvokeIfReady() + { + if (!isReady) + { + // Check if the asynchronous task is ready + if (asyncTask.valid() && asyncTask.wait_for(std::chrono::seconds(0)) == std::future_status::ready) + { + // get() rethrows anything the async function threw. Swallowing it + // would wedge the cluster handoff (the completion callback signals + // map readiness to the registry), so fail fast with context and let + // the registry's crash recovery rebalance this node. + try + { + callbackFunc(asyncTask.get()); + } + catch (std::exception const& e) + { + LOG_ERROR("server.tc9", "AsyncTask failed: {}", e.what()); + ABORT("AsyncTask failed: {}", e.what()); + } + isReady = true; + return true; + } + } + return false; + } + + void ExecuteAsync() + { + // Capture the function by value so a moved AsyncTask does not leave + // the in-flight async holding a dangling this pointer. + AsyncFunction fn = asyncFunc; + asyncTask = std::async(std::launch::async, [fn = std::move(fn)]() mutable + { + return fn(); + }); + } + +private: + AsyncFunction asyncFunc; + CallbackFunction callbackFunc; + std::shared_future asyncTask; + bool isReady; +}; + +#endif // _ASYNC_TASK_H diff --git a/src/server/game/TC9Sidecar/TC9GroupHooks.cpp b/src/server/game/TC9Sidecar/TC9GroupHooks.cpp new file mode 100644 index 000000000..939e4e47f --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GroupHooks.cpp @@ -0,0 +1,115 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "TC9GroupHooks.h" +#include "CharacterCache.h" +#include "Group.h" +#include "GroupMgr.h" +#include "Log.h" + +void ToCloud9GroupHooks::OnGroupCreated(EventObjectGroup *group) +{ + LOG_INFO("server", "Group created. ID: {}; Leader: {}.", group->guid, group->leader); + + // Idempotent under sidecar event redelivery: a replayed create must not leak a second Group. + if (sGroupMgr->GetGroupByGUID(group->guid)) + return; + + Group* g = new Group(); + g->m_guid = ObjectGuid(HighGuid::Group, group->guid); + g->m_leaderGuid = ObjectGuid(group->leader); + sCharacterCache->GetCharacterNameByGuid(g->m_leaderGuid, g->m_leaderName); + g->m_dungeonDifficulty = Difficulty(group->difficulty); + g->m_raidDifficulty = Difficulty(group->raidDifficulty); + g->m_lootMethod = LootMethod(group->lootMethod); + g->m_lootThreshold = ItemQualities(group->lootThreshold); + g->m_looterGuid = ObjectGuid(group->looterGuid); + g->m_masterLooterGuid = ObjectGuid(group->masterLooterGuid); + g->m_groupType = GroupType(group->groupType); + + // Must precede member insertion: it zeroes the subgroup counters that AddMemberWithGuid increments. + if (g->m_groupType & GROUPTYPE_RAID) + g->_initRaidSubGroupsCounter(); + + for (int i = 0; i < group->membersSize; i++) + g->AddMemberWithGuid(ObjectGuid(group->members[i])); + + sGroupMgr->AddGroup(g); + // Mark the service-assigned id used so a locally generated group can't collide with it. + sGroupMgr->RegisterGroupId(g->GetGUID().GetCounter()); +} + +void ToCloud9GroupHooks::OnGroupDisbanded(uint32 group) +{ + LOG_INFO("server", "Group disbanded. ID: {}.", group); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->ForcedDisband(true); +} + +void ToCloud9GroupHooks::OnGroupMemberAdded(uint32 group, uint64 member) +{ + LOG_INFO("server", "Group member added. ID: {}; Member: {}.", group, member); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->AddMemberWithGuid(ObjectGuid(member)); +} + +void ToCloud9GroupHooks::OnGroupMemberRemoved(uint32 group, uint64 member, uint64 newLeader) +{ + LOG_INFO("server", "Group member removed. ID: {}; Member: {}; NewLeader: {}.", group, member, newLeader); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->RemoveMember(ObjectGuid(member)); +} + +void ToCloud9GroupHooks::OnGroupLootTypeChanged(uint32 group, uint8 lootType, uint64 looter, uint8 lootThreshold) +{ + LOG_INFO("server", "Group loot type changed. ID: {}; LootType: {}; Looter: {}; LootThreshold: {}.", + group, lootType, looter, lootThreshold); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + { + g->SetLootMethod((LootMethod)lootType); + g->SetMasterLooterGuid(ObjectGuid(looter)); + g->SetLootThreshold((ItemQualities)lootThreshold); + } +} + +void ToCloud9GroupHooks::OnGroupConvertedToRaid(uint32 group) +{ + LOG_INFO("server", "Group converted to raid. ID: {}.", group); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->ConvertToRaid(); +} + +void ToCloud9GroupHooks::OnGroupRaidDifficultyChanged(uint32 group, uint8 difficulty) +{ + LOG_INFO("server", "Raid difficulty changed. ID: {}; Difficulty: {}.", group, difficulty); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->SetRaidDifficulty((Difficulty)difficulty); +} + +void ToCloud9GroupHooks::OnGroupDungeonDifficultyChanged(uint32 group, uint8 difficulty) +{ + LOG_INFO("server", "Dungeon difficulty changed. ID: {}; Difficulty: {}.", group, difficulty); + + if (Group* g = sGroupMgr->GetGroupByGUID(group)) + g->SetDungeonDifficulty((Difficulty)difficulty); +} diff --git a/src/server/game/TC9Sidecar/TC9GroupHooks.h b/src/server/game/TC9Sidecar/TC9GroupHooks.h new file mode 100644 index 000000000..2af1e342e --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GroupHooks.h @@ -0,0 +1,40 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _TC9_GROUP_HOOKS_H +#define _TC9_GROUP_HOOKS_H + +#include "Common.h" +#include "libsidecar.h" + +class ToCloud9GroupHooks +{ +public: + ToCloud9GroupHooks() {}; + ~ToCloud9GroupHooks() {}; + + static void OnGroupCreated(EventObjectGroup *group); + static void OnGroupDisbanded(uint32 group); + static void OnGroupMemberAdded(uint32 group, uint64 member); + static void OnGroupMemberRemoved(uint32 group, uint64 member, uint64 newLeader); + static void OnGroupLootTypeChanged(uint32 group, uint8 lootType, uint64 looter, uint8 lootThreshold); + static void OnGroupConvertedToRaid(uint32 group); + static void OnGroupRaidDifficultyChanged(uint32 group, uint8 difficulty); + static void OnGroupDungeonDifficultyChanged(uint32 group, uint8 difficulty); +}; + +#endif /* TC9GroupHooks_h */ diff --git a/src/server/game/TC9Sidecar/TC9GrpcHandler.cpp b/src/server/game/TC9Sidecar/TC9GrpcHandler.cpp new file mode 100644 index 000000000..90059924f --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GrpcHandler.cpp @@ -0,0 +1,374 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "TC9GrpcHandler.h" +#include "Bag.h" +#include "BattlegroundMgr.h" +#include "Item.h" +#include "ObjectAccessor.h" +#include "Player.h" + +GetPlayerItemsByGuidsResponse ToCloud9GrpcHandler::GetPlayerItemsByGuids(uint64 playerGuid, uint64* items, int itemsLen) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + GetPlayerItemsByGuidsResponse resp; + resp.errorCode = PlayerItemErrorCodePlayerNotFound; + return resp; + } + + if (itemsLen <= 0) + { + GetPlayerItemsByGuidsResponse resp; + resp.errorCode = PlayerItemErrorCodeNoError; + resp.items = nullptr; + resp.itemsSize = 0; + return resp; + } + + int itemsFound = 0; + std::unique_ptr foundItems(new Item * [itemsLen]); + for (int i = 0; i < itemsLen; i++) + { + foundItems[i] = player->GetItemByGuid(ObjectGuid(items[i])); + if (foundItems[i]) + itemsFound++; + } + + // Don't forget to delete on "that" side. + PlayerItem* itemsResult = static_cast(malloc(sizeof(PlayerItem) * itemsFound)); + int itemsResultsItr = 0; + for (int i = 0; i < itemsLen; i++) + { + if (!foundItems[i]) + continue; + + Item* pItem = foundItems[i]; + + PlayerItem item; + item.guid = pItem->GetGUID().GetRawValue(); + item.entry = pItem->GetEntry(); + item.owner = playerGuid; + item.bagSlot = pItem->GetBagSlot(); + item.slot = pItem->GetSlot(); + item.isTradable = pItem->CanBeTraded(true); + item.count = pItem->GetCount(); + item.flags = pItem->GetUInt32Value(ITEM_FIELD_FLAGS); + item.durability = pItem->GetUInt32Value(ITEM_FIELD_DURABILITY); + item.randomPropertyID = pItem->GetItemRandomPropertyId(); + + // Don't forget to delete on "that" side. + char *text = (char*)malloc(sizeof(char) * (pItem->GetText().length() + 1)); + strcpy(text, pItem->GetText().c_str()); + item.text = text; + + itemsResult[itemsResultsItr] = item; + + itemsResultsItr++; + } + + GetPlayerItemsByGuidsResponse resp; + resp.errorCode = PlayerItemErrorCodeNoError; + resp.items = itemsResult; + resp.itemsSize = itemsFound; + return resp; +} + +RemoveItemsWithGuidsFromPlayerResponse ToCloud9GrpcHandler::RemoveItemsWithGuidsFromPlayer(uint64 playerGuid, uint64* items, int itemsLen, uint64 assignToPlayerGuid) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + RemoveItemsWithGuidsFromPlayerResponse resp; + resp.errorCode = PlayerItemErrorCodePlayerNotFound; + return resp; + } + + if (itemsLen <= 0) + { + RemoveItemsWithGuidsFromPlayerResponse resp; + resp.errorCode = PlayerItemErrorCodeNoError; + resp.updatedItems = nullptr; + resp.updatedItemsSize = 0; + return resp; + } + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + int itemsFound = 0; + std::unique_ptr deletedItems(new uint64 [itemsLen]); + for (int i = 0; i < itemsLen; i++) + { + Item *item = player->GetItemByGuid(ObjectGuid(items[i])); + if (!item) + { + deletedItems[i] = 0; + continue; + } + + itemsFound++; + deletedItems[i] = item->GetGUID().GetRawValue(); + + item->SetNotRefundable(player); + player->MoveItemFromInventory(item->GetBagSlot(), item->GetSlot(), true); + + item->DeleteFromInventoryDB(trans); + item->SetOwnerGUID(ObjectGuid(assignToPlayerGuid)); + item->SetState(ITEM_CHANGED); + item->SaveToDB(trans); + + delete item; + } + + if (itemsFound > 0) + { + player->SaveInventoryAndGoldToDB(trans); + CharacterDatabase.CommitTransaction(trans); + } + + // Don't forget to delete on "that" side. + uint64_t* itemsResult = (uint64_t*)malloc(sizeof(uint64_t) * itemsFound); + int itemsResultsItr = 0; + for (int i = 0; i < itemsLen; i++) + { + if (deletedItems[i] == 0) + continue; + + itemsResult[itemsResultsItr] = deletedItems[i]; + itemsResultsItr++; + } + + RemoveItemsWithGuidsFromPlayerResponse resp; + resp.errorCode = PlayerItemErrorCodeNoError; + resp.updatedItems = itemsResult; + resp.updatedItemsSize = itemsResultsItr; + return resp; + +} + +PlayerItemErrorCode ToCloud9GrpcHandler::AddExistingItemToPlayer(AddExistingItemToPlayerRequest* request) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(request->playerGuid)); + if (!player) + return PlayerItemErrorCodePlayerNotFound; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(request->itemEntry); + if (!proto) + return PlayerItemErrorUnknownTemplate; + + Item* item = NewItemOrBag(proto); + if (!item->Create(ObjectGuid(request->itemGuid).GetCounter(), request->itemEntry, player)) + { + delete item; + return PlayerItemErrorFailedToCreateItem; + } + + item->SetUInt32Value(ITEM_FIELD_FLAGS, request->itemFlags); + item->SetUInt32Value(ITEM_FIELD_DURABILITY, request->itemDurability); + item->SetItemRandomProperties(request->itemRandomPropertyID); + item->SetCount(request->itemCount); + + // TODO: Add text. + + ItemPosCountVec dest; + uint8 msg = player->CanStoreItem(NULL_BAG, NULL_SLOT, dest, item, false); + if (msg != EQUIP_ERR_OK) + { + delete item; + return PlayerItemErrorNoInventorySpace; + } + + player->MoveItemToInventory(dest, item, true); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + player->SaveInventoryAndGoldToDB(trans); + CharacterDatabase.CommitTransaction(trans); + + return PlayerItemErrorCodeNoError; +} + +GetMoneyForPlayerResponse ToCloud9GrpcHandler::GetMoneyForPlayer(uint64 playerGuid) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + GetMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodePlayerNotFound; + return resp; + } + + GetMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodeNoError; + resp.money = player->GetMoney(); + return resp; +} + +ModifyMoneyForPlayerResponse ToCloud9GrpcHandler::ModifyMoneyForPlayer(uint64 playerGuid, int32 value) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + ModifyMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodePlayerNotFound; + return resp; + } + + if (!player->ModifyMoney(value, true)) + { + ModifyMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodeTooMuchMoney; + resp.newMoneyValue = player->GetMoney(); + return resp; + } + + ModifyMoneyForPlayerResponse resp; + resp.errorCode = PlayerMoneyErrorCodeNoError; + resp.newMoneyValue = player->GetMoney(); + return resp; +} + +CanPlayerInteractWithGOAndTypeResponse ToCloud9GrpcHandler::CanPlayerInteractWithGOAndType(uint64 playerGuid, uint64 go, uint8 goType) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + CanPlayerInteractWithGOAndTypeResponse resp; + resp.errorCode = PlayerInteractionErrorCodeCodePlayerNotFound; + return resp; + } + + CanPlayerInteractWithGOAndTypeResponse resp; + resp.errorCode = PlayerInteractionErrorCodeNoError; + resp.canInteract = player->GetGameObjectIfCanInteractWith(ObjectGuid(go), (GameobjectTypes)goType) != nullptr; + return resp; +} + +CanPlayerInteractWithNPCAndFlagsResponse ToCloud9GrpcHandler::CanPlayerInteractWithNPCAndFlags(uint64 playerGuid, uint64 npc, uint32 unitFlags) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + { + CanPlayerInteractWithNPCAndFlagsResponse resp; + resp.errorCode = PlayerInteractionErrorCodeCodePlayerNotFound; + return resp; + } + + CanPlayerInteractWithNPCAndFlagsResponse resp; + resp.errorCode = PlayerInteractionErrorCodeNoError; + resp.canInteract = player->GetNPCIfCanInteractWith(ObjectGuid(npc), (NPCFlags)unitFlags) != nullptr; + return resp; +} + +BattlegroundStartResponse ToCloud9GrpcHandler::StartBattleground(BattlegroundStartRequest* req) +{ + PvPDifficultyEntry const* pvpEntry = GetBattlegroundBracketByLevel(req->mapID, req->bracketLvl); + if (!pvpEntry) + { + BattlegroundStartResponse resp; + resp.errorCode = BattlegroundErrorFailedToCreateBG; + return resp; + } + + BattlegroundTypeId bgTypeId = BattlegroundTypeId(req->battlegroundTypeID); + + Battleground* bg = sBattlegroundMgr->CreateNewBattleground(bgTypeId, pvpEntry, req->arenaType, req->isRated); + if (!bg) + { + BattlegroundStartResponse resp; + resp.errorCode = BattlegroundErrorFailedToCreateBG; + return resp; + } + + bg->StartBattleground(); + + bg->IncreaseInvitedCount(TEAM_HORDE); + bg->IncreaseInvitedCount(TEAM_ALLIANCE); + + BattlegroundStartResponse resp; + resp.errorCode = BattlegroundErrorCodeNoError; + resp.instanceID = bg->GetInstanceID(); + resp.instanceClientID = bg->GetClientInstanceID(); + return resp; +} + +BattlegroundErrorCode ToCloud9GrpcHandler::AddPlayersToBattleground(BattlegroundAddPlayersRequest* request) +{ + BattlegroundTypeId bgTypeId = BattlegroundTypeId(request->battlegroundTypeID); + + Battleground* bg = sBattlegroundMgr->GetBattleground(request->instanceID, BATTLEGROUND_TYPE_NONE); + if (!bg) + return BattlegroundErrorBattlegroundNotFound; + + for (int i = 0; i < request->alliancePlayersToAddSize; i++) + { + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(request->alliancePlayersToAdd[i])); + if (player) + { + player->SetEntryPoint(); + player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), 1, true, bgTypeId == BATTLEGROUND_RB, player->GetTeamId(true)); + sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId); + } + } + + for (int i = 0; i < request->hordePlayersToAddSize; i++) + { + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(request->hordePlayersToAdd[i])); + if (player) + { + player->SetEntryPoint(); + player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), 1, true, bgTypeId == BATTLEGROUND_RB, player->GetTeamId(true)); + sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId); + } + } + + return BattlegroundErrorCodeNoError; +} + +BattlegroundJoinCheckErrorCode ToCloud9GrpcHandler::CanPlayerJoinBattlegroundQueue(uint64 playerGuid) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + return BattlegroundJoinCheckErrorCodePlayerNotFound; + + // Lets ignore RBAC checks for now. + Battleground* bg = sBattlegroundMgr->GetBattlegroundTemplate(BATTLEGROUND_RB); + if (!bg) + return BattlegroundJoinCheckErrorCodeResponseIsFalse; + + // has deserter debuff + if (!player->CanJoinToBattleground(bg)) + return BattlegroundJoinCheckErrorCodeResponseIsFalse; + + // don't let Death Knights join BG queues when they are not allowed to be teleported yet + if (player->IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TELEPORT) && player->GetMapId() == 609 && !player->IsGameMaster() && !player->HasSpell(50977)) + return BattlegroundJoinCheckErrorCodeResponseIsFalse; + + return BattlegroundJoinCheckErrorCodeOK; +} + +BattlegroundJoinCheckErrorCode ToCloud9GrpcHandler::CanPlayerTeleportToBattleground(uint64 playerGuid) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(playerGuid)); + if (!player) + return BattlegroundJoinCheckErrorCodePlayerNotFound; + + if (player->GetCharmGUID() || player->IsInCombat()) + return BattlegroundJoinCheckErrorCodeResponseIsFalse; + + return BattlegroundJoinCheckErrorCodeOK; +} diff --git a/src/server/game/TC9Sidecar/TC9GrpcHandler.h b/src/server/game/TC9Sidecar/TC9GrpcHandler.h new file mode 100644 index 000000000..438ea6550 --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GrpcHandler.h @@ -0,0 +1,50 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _TC9_GRPC_HANDLER_H +#define _TC9_GRPC_HANDLER_H + +#include "Common.h" +#include "libsidecar.h" + +class ToCloud9GrpcHandler +{ +public: + ToCloud9GrpcHandler() {}; + ~ToCloud9GrpcHandler() {}; + + // Items + static GetPlayerItemsByGuidsResponse GetPlayerItemsByGuids(uint64 player, uint64* items, int items_len); + static RemoveItemsWithGuidsFromPlayerResponse RemoveItemsWithGuidsFromPlayer(uint64 player, uint64* items, int itemsLen, uint64 assignToPlayer); + static PlayerItemErrorCode AddExistingItemToPlayer(AddExistingItemToPlayerRequest*); + + // Money + static GetMoneyForPlayerResponse GetMoneyForPlayer(uint64 player); + static ModifyMoneyForPlayerResponse ModifyMoneyForPlayer(uint64 player, int32 value); + + // Interactions + static CanPlayerInteractWithGOAndTypeResponse CanPlayerInteractWithGOAndType(uint64 player, uint64 go, uint8 goType); + static CanPlayerInteractWithNPCAndFlagsResponse CanPlayerInteractWithNPCAndFlags(uint64 player, uint64 npc, uint32 unitFlags); + + // Battlegrounds + static BattlegroundStartResponse StartBattleground(BattlegroundStartRequest* request); + static BattlegroundErrorCode AddPlayersToBattleground(BattlegroundAddPlayersRequest* request); + static BattlegroundJoinCheckErrorCode CanPlayerJoinBattlegroundQueue(uint64 player); + static BattlegroundJoinCheckErrorCode CanPlayerTeleportToBattleground(uint64 player); +}; + +#endif // _TC9_GRPC_HANDLER_H diff --git a/src/server/game/TC9Sidecar/TC9GuildHooks.cpp b/src/server/game/TC9Sidecar/TC9GuildHooks.cpp new file mode 100644 index 000000000..73abcb89b --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GuildHooks.cpp @@ -0,0 +1,47 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "TC9GuildHooks.h" +#include "ObjectAccessor.h" +#include "Player.h" + +void ToCloud9GuildHooks::OnGuildMemberAdded(uint64 guild, uint64 character) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(character)); + if (!player) + return; + + player->SetInGuild(guild); +} + +void ToCloud9GuildHooks::OnGuildMemberRemoved(uint64 /*guild*/, uint64 character) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(character)); + if (!player) + return; + + player->SetInGuild(0); +} + +void ToCloud9GuildHooks::OnGuildMemberLeft(uint64 /*guild*/, uint64 character) +{ + Player *player = ObjectAccessor::FindPlayer(ObjectGuid(character)); + if (!player) + return; + + player->SetInGuild(0); +} diff --git a/src/server/game/TC9Sidecar/TC9GuildHooks.h b/src/server/game/TC9Sidecar/TC9GuildHooks.h new file mode 100644 index 000000000..e02da31fd --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9GuildHooks.h @@ -0,0 +1,34 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _TC9_GUILD_HOOKS_H +#define _TC9_GUILD_HOOKS_H + +#include "Common.h" + +class ToCloud9GuildHooks +{ +public: + ToCloud9GuildHooks() {}; + ~ToCloud9GuildHooks() {}; + + static void OnGuildMemberAdded(uint64 guild, uint64 character); + static void OnGuildMemberRemoved(uint64 guild, uint64 character); + static void OnGuildMemberLeft(uint64 guild, uint64 character); +}; + +#endif // _TC9_GUILD_HOOKS_H diff --git a/src/server/game/TC9Sidecar/TC9Sidecar.cpp b/src/server/game/TC9Sidecar/TC9Sidecar.cpp new file mode 100644 index 000000000..ef808a9c5 --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9Sidecar.cpp @@ -0,0 +1,268 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "TC9Sidecar.h" +#include "Config.h" +#include "InstanceSaveMgr.h" +#include "libsidecar.h" +#include "Log.h" +#include "MapMgr.h" +#include "Player.h" +#include "TC9GroupHooks.h" +#include "TC9GrpcHandler.h" +#include "TC9GuildHooks.h" +#include "UpdateTime.h" +#include "WorldSessionMgr.h" + +#include + +#define AVAILABLE_MAPS_ALL_MAPS "" + +MonitoringDataCollectorResponse HandleMonitoringRequest(); + +ToCloud9Sidecar* ToCloud9Sidecar::instance() +{ + static ToCloud9Sidecar instance; + return &instance; +} + +ToCloud9Sidecar::ToCloud9Sidecar() : _clusterModeEnabled(false), _isCrossrealm(false) +{ +} + +void ToCloud9Sidecar::Init(uint16 port, int realmId) +{ + _clusterModeEnabled = sConfigMgr->GetOption("Cluster.Enabled", false); + + if (_clusterModeEnabled) + { + uint32 *assignedMaps; + int assignedMapsSize = 0; + + _isCrossrealm = sConfigMgr->GetOption("Cluster.IsCrossrealm", false); + + std::string availableMaps = sConfigMgr->GetOption("Cluster.AvailableMaps", AVAILABLE_MAPS_ALL_MAPS); + TC9InitLib(port, realmId, _isCrossrealm, availableMaps.data(), &assignedMaps, &assignedMapsSize); + + for (int i = 0; i < MAX_MAP_ID; i++) + _assignedMapsByID[i] = false; + + for (int i = 0; i < assignedMapsSize; i++) + { + uint32 mapId = assignedMaps[i]; + if (mapId >= MAX_MAP_ID) + { + LOG_ERROR("server", "ToCloud9Sidecar::Init: map id {} out of range [0, {}), ignored", + mapId, MAX_MAP_ID); + continue; + } + _assignedMapsByID[mapId] = true; + } + + if (assignedMapsSize > 0) + free(assignedMaps); + + SetupHooks(); + SetupGrpcHandlers(); + } +} + +void ToCloud9Sidecar::Deinit() +{ + if (_clusterModeEnabled) + TC9GracefulShutdown(); +} + +void ToCloud9Sidecar::SetupHooks() +{ + TC9SetOnMapsReassignedHook(&ToCloud9Sidecar::OnMapsReassigned); + + TC9SetOnGuildMemberLeftHook(&ToCloud9GuildHooks::OnGuildMemberLeft); + TC9SetOnGuildMemberAddedHook(&ToCloud9GuildHooks::OnGuildMemberAdded); + TC9SetOnGuildMemberRemovedHook(&ToCloud9GuildHooks::OnGuildMemberRemoved); + + TC9SetOnGroupCreatedHook(&ToCloud9GroupHooks::OnGroupCreated); + TC9SetOnGroupDisbandedHook(&ToCloud9GroupHooks::OnGroupDisbanded); + TC9SetOnGroupMemberAddedHook(&ToCloud9GroupHooks::OnGroupMemberAdded); + TC9SetOnGroupMemberRemovedHook(&ToCloud9GroupHooks::OnGroupMemberRemoved); + TC9SetOnGroupLootTypeChangedHook(&ToCloud9GroupHooks::OnGroupLootTypeChanged); + TC9SetOnGroupConvertedToRaidHook(&ToCloud9GroupHooks::OnGroupConvertedToRaid); + TC9SetOnGroupRaidDifficultyChangedHook(&ToCloud9GroupHooks::OnGroupRaidDifficultyChanged); + TC9SetOnGroupDungeonDifficultyChangedHook(&ToCloud9GroupHooks::OnGroupDungeonDifficultyChanged); +} + +void ToCloud9Sidecar::SetupGrpcHandlers() +{ + TC9SetGetPlayerItemsByGuidsHandler(&ToCloud9GrpcHandler::GetPlayerItemsByGuids); + TC9SetRemoveItemsWithGuidsFromPlayerHandler(&ToCloud9GrpcHandler::RemoveItemsWithGuidsFromPlayer); + TC9SetAddExistingItemToPlayerHandler(&ToCloud9GrpcHandler::AddExistingItemToPlayer); + + TC9SetGetMoneyForPlayerHandler(&ToCloud9GrpcHandler::GetMoneyForPlayer); + TC9SetModifyMoneyForPlayerHandler(&ToCloud9GrpcHandler::ModifyMoneyForPlayer); + + TC9SetCanPlayerInteractWithGOAndTypeHandler(&ToCloud9GrpcHandler::CanPlayerInteractWithGOAndType); + TC9SetCanPlayerInteractWithNPCAndFlagsHandler(&ToCloud9GrpcHandler::CanPlayerInteractWithNPCAndFlags); + + TC9SetBattlegroundStartHandler(&ToCloud9GrpcHandler::StartBattleground); + TC9SetBattlegroundAddPlayersHandler(&ToCloud9GrpcHandler::AddPlayersToBattleground); + TC9SetCanPlayerJoinBattlegroundQueueHandler(&ToCloud9GrpcHandler::CanPlayerJoinBattlegroundQueue); + TC9SetCanPlayerTeleportToBattlegroundHandler(&ToCloud9GrpcHandler::CanPlayerTeleportToBattleground); + + TC9SetMonitoringDataCollectorHandler(&HandleMonitoringRequest); +} + +void ToCloud9Sidecar::ProcessHooks() +{ + TC9ProcessEventsHooks(); +} + +void ToCloud9Sidecar::ProcessGrpcOrHttpRequests() +{ + TC9ProcessGRPCOrHTTPRequests(); +} + +void ToCloud9Sidecar::ProcessAsyncTasks() +{ + _asyncTasksProcessor.ProcessReadyCallbacks(); +} + +bool ToCloud9Sidecar::IsMapAssigned(uint32 mapId) +{ + if (mapId >= MAX_MAP_ID) + return false; + + return _assignedMapsByID[mapId]; +} + +uint32 ToCloud9Sidecar::GenerateCharacterGuid(uint16 realmId) +{ + return uint32(TC9GetNextAvailableCharacterGuid(realmId)); +} + +uint32 ToCloud9Sidecar::GenerateItemGuid(uint16 realmId) +{ + return uint32(TC9GetNextAvailableItemGuid(realmId)); +} + +uint32 ToCloud9Sidecar::GenerateInstanceGuid(uint16 realmId) +{ + return uint32(TC9GetNextAvailableInstanceGuid(realmId)); +} + +void ToCloud9Sidecar::OnPlayerLeftBattleground(uint64 playerGUID, uint32 realmID, uint32 instanceID) +{ + TC9PlayerLeftBattleground(playerGUID, realmID, instanceID); +} + +void ToCloud9Sidecar::OnBattlegroundStatusChanged(uint32 instanceID, uint8 status) +{ + TC9BattlegroundStatusChanged(instanceID, status); +} + +bool ToCloud9Sidecar::NatsPublish(std::string const& subject, std::string const& payload) +{ + if (!_clusterModeEnabled) + return false; + + if (payload.size() > size_t(std::numeric_limits::max())) + return false; + + return TC9NatsPublish(subject.c_str(), payload.c_str(), int(payload.size())) == 0; +} + +bool ToCloud9Sidecar::NatsSubscribe(std::string const& subject, void (*handler)(char const*, char const*, int)) +{ + if (!_clusterModeEnabled || !handler) + return false; + + return TC9NatsSubscribe(subject.c_str(), handler) == 0; +} + +void ToCloud9Sidecar::OnMapsReassigned(uint32* addedMaps, int addedMapsSize, uint32* removedMaps, int removedMapsSize) +{ + std::vector newMapIDs; + newMapIDs.reserve(addedMapsSize > 0 ? addedMapsSize : 0); + + for (int i = 0; i < addedMapsSize; i++) + { + uint32 mapId = addedMaps[i]; + if (mapId >= MAX_MAP_ID) + { + LOG_ERROR("server", "ToCloud9Sidecar::OnMapsReassigned: added map id {} out of range [0, {}), ignored", + mapId, MAX_MAP_ID); + continue; + } + + sToCloud9Sidecar->_assignedMapsByID[mapId] = true; + newMapIDs.push_back(mapId); + + if (Map* map = sMapMgr->FindBaseNonInstanceMap(mapId)) + map->StopPlayersRedirectKickTimer(); + } + + for (int i = 0; i < removedMapsSize; i++) + { + uint32 mapId = removedMaps[i]; + if (mapId >= MAX_MAP_ID) + { + LOG_ERROR("server", "ToCloud9Sidecar::OnMapsReassigned: removed map id {} out of range [0, {}), ignored", + mapId, MAX_MAP_ID); + continue; + } + + sToCloud9Sidecar->_assignedMapsByID[mapId] = false; + + if (Map* map = sMapMgr->FindBaseNonInstanceMap(mapId)) + map->StartPlayersRedirectKickTimer(); + } + + if (!newMapIDs.empty()) + { + auto loadRowsPtr = std::make_shared(); + + AsyncTask task( + [loadRowsPtr, newMapIDs]() -> bool { + LOG_INFO("server", "Starting to load data for newly assigned maps..."); + + *loadRowsPtr = sInstanceSaveMgr->LoadInstanceSavesAndBindsForMapIDs(newMapIDs); + return true; + }, + [loadRowsPtr, newMapIDs](bool) { + sInstanceSaveMgr->MergeWithNewInstanceSaves(*loadRowsPtr); + TC9ReadyToAcceptPlayersFromMaps((uint32_t*)newMapIDs.data(), newMapIDs.size()); + + LOG_INFO("server", "Finished loading data for newly assigned maps."); + } + ); + + task.ExecuteAsync(); + sToCloud9Sidecar->_asyncTasksProcessor.AddCallback(std::move(task)); + } +} + +MonitoringDataCollectorResponse HandleMonitoringRequest() +{ + MonitoringDataCollectorResponse res; + res.errorCode = MonitoringErrorCodeNoError; + res.diffMean = sWorldUpdateTime.GetAverageUpdateTime(); + res.diffMedian = sWorldUpdateTime.GetPercentile(50); + res.diff95Percentile = sWorldUpdateTime.GetPercentile(95); + res.diff99Percentile = sWorldUpdateTime.GetPercentile(99); + res.diffMaxPercentile = sWorldUpdateTime.GetPercentile(100); + res.connectedPlayers = sWorldSessionMgr->GetActiveSessionCount(); + return res; +} diff --git a/src/server/game/TC9Sidecar/TC9Sidecar.h b/src/server/game/TC9Sidecar/TC9Sidecar.h new file mode 100644 index 000000000..417c88914 --- /dev/null +++ b/src/server/game/TC9Sidecar/TC9Sidecar.h @@ -0,0 +1,78 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _TC9_SIDECAR_H +#define _TC9_SIDECAR_H + +#include "AsyncCallbackProcessor.h" +#include "AsyncTask.h" +#include "Common.h" +#include "ObjectGuid.h" + +#define MAX_MAP_ID 800 // Probably too much, but let's lean towards caution. + +class ToCloud9Sidecar +{ +private: + ToCloud9Sidecar(); + ~ToCloud9Sidecar() {}; + +public: + static ToCloud9Sidecar* instance(); + + void Init(uint16 port, int realmId); + void Deinit(); + + bool ClusterModeEnabled() { return _clusterModeEnabled; } + bool IsCrossrealm() { return _isCrossrealm; } + + bool IsMapAssigned(uint32 mapId); + + void SetupHooks(); + void SetupGrpcHandlers(); + + void ProcessHooks(); + void ProcessGrpcOrHttpRequests(); + void ProcessAsyncTasks(); + + uint32 GenerateCharacterGuid(uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID); + uint32 GenerateItemGuid(uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID); + uint32 GenerateInstanceGuid(uint16 realmId = DEFAULT_NON_CROSSREALM_REALM_ID); + + void OnPlayerLeftBattleground(uint64 playerGUID, uint32 realmID, uint32 instanceID); + void OnBattlegroundStatusChanged(uint32 instanceID, uint8 status); + + // Generic NATS pub/sub (single choke point for in-process modules). + // No-ops outside cluster mode. Subscribe callbacks run on the world + // thread (ProcessHooks). + bool NatsPublish(std::string const& subject, std::string const& payload); + bool NatsSubscribe(std::string const& subject, void (*handler)(char const* subject, char const* payload, int payloadLen)); + +private: + static void OnMapsReassigned(uint32* addedMaps, int addedMapsSize, uint32* removedMaps, int removedMapsSize); + + bool _clusterModeEnabled; + bool _isCrossrealm; + + bool _assignedMapsByID[MAX_MAP_ID]; + + AsyncCallbackProcessor> _asyncTasksProcessor; +}; + +#define sToCloud9Sidecar ToCloud9Sidecar::instance() + +#endif // _TC9_SIDECAR_H diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index ad54e8364..28351daab 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -23,8 +23,8 @@ #include "AccountMgr.h" #include "AchievementMgr.h" #include "AddonMgr.h" -#include "ArenaTeamMgr.h" #include "ArenaSeasonMgr.h" +#include "ArenaTeamMgr.h" #include "AuctionHouseMgr.h" #include "AutobroadcastMgr.h" #include "BattlefieldMgr.h" @@ -53,8 +53,8 @@ #include "GridNotifiersImpl.h" #include "GroupMgr.h" #include "GuildMgr.h" -#include "IPLocation.h" #include "InstanceSaveMgr.h" +#include "IPLocation.h" #include "ItemEnchantmentMgr.h" #include "LFGMgr.h" #include "Language.h" @@ -82,6 +82,7 @@ #include "SmartAI.h" #include "SpellMgr.h" #include "TaskScheduler.h" +#include "TC9Sidecar.h" #include "TicketMgr.h" #include "Transport.h" #include "TransportMgr.h" @@ -1341,6 +1342,24 @@ void World::Update(uint32 diff) sScriptMgr->OnWorldUpdate(diff); } + if (sToCloud9Sidecar->ClusterModeEnabled()) + { + { + METRIC_TIMER("world_update_time", METRIC_TAG("type", "Process TC9 async tasks")); + sToCloud9Sidecar->ProcessAsyncTasks(); + } + + { + METRIC_TIMER("world_update_time", METRIC_TAG("type", "Process TC9 hooks")); + sToCloud9Sidecar->ProcessHooks(); + } + + { + METRIC_TIMER("world_update_time", METRIC_TAG("type", "Process TC9 gRPC and HTTP requests")); + sToCloud9Sidecar->ProcessGrpcOrHttpRequests(); + } + } + { METRIC_TIMER("world_update_time", METRIC_TAG("type", "Update metrics")); // Stats logger update diff --git a/src/server/game/World/WorldState.cpp b/src/server/game/World/WorldState.cpp index 269e53cc7..5441466b7 100644 --- a/src/server/game/World/WorldState.cpp +++ b/src/server/game/World/WorldState.cpp @@ -22,6 +22,7 @@ #include "MapMgr.h" #include "Player.h" #include "SharedDefines.h" +#include "TC9Sidecar.h" #include "UnitAI.h" #include "Weather.h" #include "WorldState.h" @@ -160,20 +161,27 @@ void WorldState::LoadWorldStates() // Setting a worldstate will save it to DB void WorldState::setWorldState(uint32 index, uint64 timeValue) { - auto const& it = _worldstates.find(index); - if (it != _worldstates.end()) + // Crossrealm nodes must not persist worldstates: their CharacterDatabase is + // the routing proxy and the write would land in an arbitrary realm DB. The + // in-memory value still has to be updated so read-modify-write users + // (e.g. the Wintergrasp clock) keep working. + if (!sToCloud9Sidecar->IsCrossrealm()) { - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_WORLDSTATE); - stmt->SetData(0, uint32(timeValue)); - stmt->SetData(1, index); - CharacterDatabase.Execute(stmt); - } - else - { - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_WORLDSTATE); - stmt->SetData(0, index); - stmt->SetData(1, uint32(timeValue)); - CharacterDatabase.Execute(stmt); + auto const& it = _worldstates.find(index); + if (it != _worldstates.end()) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_WORLDSTATE); + stmt->SetData(0, uint32(timeValue)); + stmt->SetData(1, index); + CharacterDatabase.Execute(stmt); + } + else + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_WORLDSTATE); + stmt->SetData(0, index); + stmt->SetData(1, uint32(timeValue)); + CharacterDatabase.Execute(stmt); + } } _worldstates[index] = timeValue; diff --git a/src/server/scripts/CMakeLists.txt b/src/server/scripts/CMakeLists.txt index e2320d251..51ab9a08f 100644 --- a/src/server/scripts/CMakeLists.txt +++ b/src/server/scripts/CMakeLists.txt @@ -232,7 +232,8 @@ target_link_libraries(scripts PRIVATE acore-core-interface PUBLIC - game-interface) + game-interface + libsidecar) target_include_directories(scripts PUBLIC From b7387f653d24ab5f93c91735b8d91fa6d629d489 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:31:00 -0300 Subject: [PATCH 41/69] feat(Core/Player): restore additional saves behind a config bitmask (#26825) --- .../apps/worldserver/worldserver.conf.dist | 14 ++++++ .../game/Achievements/AchievementMgr.cpp | 2 + src/server/game/Entities/Player/Player.h | 4 +- .../game/Entities/Player/PlayerStorage.cpp | 12 +++++ .../game/Entities/Player/PlayerUpdates.cpp | 48 +++++++++++++++++++ src/server/game/World/WorldConfig.cpp | 1 + src/server/game/World/WorldConfig.h | 1 + 7 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index d3b9db4c1..ff176fa0c 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -1838,6 +1838,20 @@ PlayerSave.Stats.MinLevel = 0 PlayerSave.Stats.SaveOnlyOnLogout = 1 +# +# PlayerSave.AdditionalSaves +# Description: Bitmask of player data to save to the database a few seconds after +# important changes instead of waiting for the next periodic save +# (PlayerSaveInterval). Reduces the progress lost on a server crash. +# Combine the values to enable multiple triggers. +# Values: 1 - Inventory and gold (after looting an item of rare or better quality) +# 2 - Quest status (after quest status changes) +# 4 - Achievements (after completing an achievement) +# Default: 0 - (Disabled) +# 7 - (All of the above) + +PlayerSave.AdditionalSaves = 0 + # # CleanCharacterDB # Description: Clean out deprecated achievements, skills, spells and talents from the db. diff --git a/src/server/game/Achievements/AchievementMgr.cpp b/src/server/game/Achievements/AchievementMgr.cpp index 55cb41b42..210d7818e 100644 --- a/src/server/game/Achievements/AchievementMgr.cpp +++ b/src/server/game/Achievements/AchievementMgr.cpp @@ -2333,6 +2333,8 @@ void AchievementMgr::CompletedAchievement(AchievementEntry const* achievement) } } + _player->AdditionalSavingAddMask(ADDITIONAL_SAVING_ACHIEVEMENTS); + if (achievement->flags & (ACHIEVEMENT_FLAG_REALM_FIRST_REACH | ACHIEVEMENT_FLAG_REALM_FIRST_KILL) && !_player->GetSession()->HasPermission(rbac::RBAC_PERM_CANNOT_EARN_REALM_FIRST_ACHIEVEMENTS)) sAchievementMgr->SetRealmCompleted(achievement); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index fe93e67b3..f62853f9e 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -988,6 +988,7 @@ enum AdditionalSaving ADDITIONAL_SAVING_NONE = 0x00, ADDITIONAL_SAVING_INVENTORY_AND_GOLD = 0x01, ADDITIONAL_SAVING_QUEST_STATUS = 0x02, + ADDITIONAL_SAVING_ACHIEVEMENTS = 0x04, }; enum PlayerCommandStates @@ -2618,7 +2619,7 @@ public: bool IsFreeFlying() const { return HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) || HasAuraType(SPELL_AURA_FLY); } // saving - void AdditionalSavingAddMask(uint8 mask) { m_additionalSaveTimer = 2000; m_additionalSaveMask |= mask; } + void AdditionalSavingAddMask(uint8 mask); // arena spectator [[nodiscard]] bool IsSpectator() const { return m_ExtraFlags & PLAYER_EXTRA_SPECTATOR_ON; } void SetIsSpectator(bool on); @@ -2814,6 +2815,7 @@ protected: void _SaveCharacter(bool create, CharacterDatabaseTransaction trans); void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction trans); void _SavePlayerSettings(CharacterDatabaseTransaction trans); + void UpdateAdditionalSaves(uint32 p_time); /*********************************************************/ /*** ENVIRONMENTAL SYSTEM ***/ diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 7083bfbd3..58cd4196e 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -7254,6 +7254,18 @@ void Player::SaveToDB(CharacterDatabaseTransaction trans, bool create, bool logo pet->SavePetToDB(PET_SAVE_AS_CURRENT); } +// flag data to be saved by UpdateAdditionalSaves a moment after an important change, +// filtered by the PlayerSave.AdditionalSaves config mask +void Player::AdditionalSavingAddMask(uint8 mask) +{ + mask &= sWorld->getIntConfig(CONFIG_ADDITIONAL_SAVES); + if (!mask) + return; + + m_additionalSaveTimer = 2000; + m_additionalSaveMask |= mask; +} + // fast save function for item/money cheating preventing - save only inventory and money state void Player::SaveInventoryAndGoldToDB(CharacterDatabaseTransaction trans) { diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index 1ba0916f2..3aa4894e9 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -332,6 +332,8 @@ void Player::Update(uint32 p_time) } } + UpdateAdditionalSaves(p_time); + // Handle Water/drowning HandleDrowning(p_time); @@ -2399,3 +2401,49 @@ void Player::ProcessSpellQueue() break; } } + +// save only the data flagged by AdditionalSavingAddMask shortly after +// important changes, so a crash loses at most a few seconds of them +void Player::UpdateAdditionalSaves(uint32 p_time) +{ + if (!m_additionalSaveTimer || GetSession()->isLogingOut()) + return; + + if (m_additionalSaveTimer > p_time) + { + m_additionalSaveTimer -= p_time; + return; + } + + uint8 mask = m_additionalSaveMask; + m_additionalSaveTimer = 0; + m_additionalSaveMask = 0; + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + if (mask & ADDITIONAL_SAVING_INVENTORY_AND_GOLD) + SaveInventoryAndGoldToDB(trans); + + if (mask & ADDITIONAL_SAVING_QUEST_STATUS) + { + _SaveQuestStatus(trans); + + // if nothing changed, nothing will happen + _SaveDailyQuestStatus(trans); + _SaveWeeklyQuestStatus(trans); + _SaveSeasonalQuestStatus(trans); + _SaveMonthlyQuestStatus(trans); + } + + if (mask & ADDITIONAL_SAVING_ACHIEVEMENTS) + { + m_achievementMgr->SaveToDB(trans); + + // achievements are often earned together with skill or gold changes + // (professions, riding, wealth), save those too to keep the DB consistent + _SaveSkills(trans); + SaveGoldToDB(trans); + } + + CharacterDatabase.CommitTransaction(trans); +} diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index bd6f0a300..0990520f7 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -168,6 +168,7 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 900000); SetConfigValue(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0); SetConfigValue(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true); + SetConfigValue(CONFIG_ADDITIONAL_SAVES, "PlayerSave.AdditionalSaves", 0); SetConfigValue(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true); SetConfigValue(CONFIG_MIN_LEVEL_STAT_SAVE, "PlayerSave.Stats.MinLevel", 0, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value < MAX_LEVEL; }, "< MAX_LEVEL"); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index d4e3c79fa..56792c005 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -27,6 +27,7 @@ enum ServerConfigs CONFIG_ALLOW_PLAYER_COMMANDS, CONFIG_CLEAN_CHARACTER_DB, CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, + CONFIG_ADDITIONAL_SAVES, CONFIG_ALLOW_TWO_SIDE_ACCOUNTS, CONFIG_ALLOW_TWO_SIDE_INTERACTION_CALENDAR, CONFIG_ALLOW_TWO_SIDE_INTERACTION_CHAT, From f29869536b77b02dfac5c5a91e3b10d51d7a2359 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:31:47 -0300 Subject: [PATCH 42/69] fix(Core/SmartAI): Don't add UNIT_STATE_MELEE_ATTACKING if unit is in ranged mode and not chasing (#26796) --- src/server/game/AI/SmartScripts/SmartAI.cpp | 25 ++++++++++++++++++++- src/server/game/AI/SmartScripts/SmartAI.h | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index bdbb5cba5..560b5624e 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -596,7 +596,25 @@ void SmartAI::UpdateAI(uint32 diff) return; if (mCanAutoAttack) + { + UpdateMeleeStance(); DoMeleeAttackIfReady(); + } +} + +void SmartAI::UpdateMeleeStance() +{ + // Ranged creatures should not switch to melee stance at distance + if (!_currentRangeMode || me->IsCrowdControlled()) + return; + + Unit* victim = me->GetVictim(); + if (!victim) + return; + + bool const canMelee = me->IsWithinMeleeRange(victim); + if (canMelee != me->HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + me->Attack(victim, canMelee); } bool SmartAI::IsEscortInvokerInRange() @@ -927,7 +945,7 @@ void SmartAI::AttackStart(Unit* who) return; } - if (who && me->Attack(who, mCanAutoAttack)) + if (who && me->Attack(who, mCanAutoAttack && !_currentRangeMode)) { if (!me->HasUnitState(UNIT_STATE_NO_COMBAT_MOVEMENT)) { @@ -1202,7 +1220,12 @@ void SmartAI::SetCurrentRangeMode(bool on, float range) _attackDistance = range; if (Unit* victim = me->GetVictim()) + { me->GetMotionMaster()->MoveChase(victim, _attackDistance); + + if (!on && mCanAutoAttack && !me->HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + me->Attack(victim, true); + } } void SmartAI::SetMainSpell(uint32 spellId) diff --git a/src/server/game/AI/SmartScripts/SmartAI.h b/src/server/game/AI/SmartScripts/SmartAI.h index d84c72929..bb4b8f6ea 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.h +++ b/src/server/game/AI/SmartScripts/SmartAI.h @@ -265,6 +265,7 @@ private: uint32 mDespawnState; void UpdateDespawn(const uint32 diff); void UpdateFollow(const uint32 diff); + void UpdateMeleeStance(); uint32 mEscortInvokerCheckTimer; bool mJustReset; From b0afc4fca8d7e719d815ebbfcecf031a476a9bf3 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:32:15 +0200 Subject: [PATCH 43/69] chore(Docs): Update README to remove unnecessary badges (#26840) --- .github/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/README.md b/.github/README.md index 3a9e654d6..3a22b60cf 100644 --- a/.github/README.md +++ b/.github/README.md @@ -2,9 +2,7 @@ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![CodeFactor](https://www.codefactor.io/repository/github/azerothcore/azerothcore-wotlk/badge)](https://www.codefactor.io/repository/github/azerothcore/azerothcore-wotlk) -[![StackOverflow](http://img.shields.io/badge/stackoverflow-azerothcore-blue.svg?logo=stackoverflow)](https://stackoverflow.com/questions/tagged/azerothcore?sort=newest "Ask / browse questions here") [![Discord](https://img.shields.io/discord/217589275766685707?logo=discord&logoColor=white)](https://discord.gg/gkt4y2x "Our community hub on Discord") -[![Bounties on BountyHub](https://img.shields.io/badge/Bounties-on%20BountyHub-yellow)](https://www.bountyhub.dev/bounties?repo=azerothcore) ## Build Status From 9f66598caa0334aa5f2a00cd3aa1b917c10c3d98 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:48:37 -0300 Subject: [PATCH 44/69] fix(Scripts/Ulduar): fire Mimiron's Rocket Strike non-triggered so the cast reaches the client (#26841) --- src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 2 +- 1 file changed, 1 insertion(+), 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 04514be9c..92581a93e 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -1359,7 +1359,7 @@ struct npc_ulduar_vx001 : public ScriptedAI _events.Repeat(10s); break; case EVENT_SPELL_ROCKET_STRIKE: - me->CastSpell(me, _phase == 2 ? SPELL_ROCKET_STRIKE_SINGLE : SPELL_ROCKET_STRIKE_BOTH, true); + me->CastSpell(me, _phase == 2 ? SPELL_ROCKET_STRIKE_SINGLE : SPELL_ROCKET_STRIKE_BOTH); _events.Repeat(20s); _events.ScheduleEvent(EVENT_REINSTALL_ROCKETS, 10s); break; From 685fb01f0403bfbe4dc3b9ccb7faa45a2def9b51 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:18:34 -0300 Subject: [PATCH 45/69] fix(DB/Quest): Use Correct Spell for Scalawag Point (#26842) --- .../rev_1785193948968943800.sql | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785193948968943800.sql diff --git a/data/sql/updates/pending_db_world/rev_1785193948968943800.sql b/data/sql/updates/pending_db_world/rev_1785193948968943800.sql new file mode 100644 index 000000000..3f4521c92 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785193948968943800.sql @@ -0,0 +1,24 @@ +-- +UPDATE `spell_area` SET `spell` = 44018 WHERE `spell` = 44017 AND `area` = 3990 AND `quest_start` = 11504 AND `aura_spell` = 0 AND `racemask` = 0 AND `gender` = 2; + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2471300); +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 +(2471300, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Remove Npc Flags Gossip'), +(2471300, 9, 1, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Say Line'), +(2471300, 9, 2, 0, 0, 0, 100, 0, 6000, 6000, 0, 0, 0, 0, 2, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Set Faction 14'), +(2471300, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Start Attacking'), +(2471300, 9, 4, 0, 0, 0, 100, 0, 30000, 30000, 0, 0, 0, 0, 82, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Add Npc Flags Gossip'), +(2471300, 9, 5, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 2, 1888, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - Actionlist - Set Faction 1888'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 24713); +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 +(24713, 0, 0, 0, 25, 0, 100, 0, 0, 0, 0, 0, 0, 0, 82, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - On Reset - Add Npc Flags Gossip'), +(24713, 0, 1, 0, 25, 0, 100, 0, 0, 0, 0, 0, 0, 0, 2, 1888, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - On Reset - Set Faction 1888'), +(24713, 0, 2, 0, 62, 0, 100, 0, 9335, 0, 0, 0, 0, 0, 80, 2471300, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Crowleg" Dan - On Gossip Option Selected - Run Script'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2453900); +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 +(2453900, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '\'Silvermoon\' Harry - On Script - Say Line 0'), +(2453900, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 83, 131, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Silvermoon" Harry - Actionlist - Remove Npc Flags Gossip & Questgiver & Vendor'), +(2453900, 9, 2, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 2, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, '"Silvermoon" Harry - Actionlist - Set Faction 14'), +(2453900, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 21, 10, 0, 0, 0, 0, 0, 0, 0, '"Silvermoon" Harry - Actionlist - Start Attacking'); From 4d40ffbab7d9c7b477bae2fef00c3a338f32cb5a Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:19:28 -0300 Subject: [PATCH 46/69] fix(DB/Creature): Add Abdul the Insane and related creatures from The Staff of Storm's Fury (#26843) --- .../rev_1785197903056451700.sql | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785197903056451700.sql diff --git a/data/sql/updates/pending_db_world/rev_1785197903056451700.sql b/data/sql/updates/pending_db_world/rev_1785197903056451700.sql new file mode 100644 index 000000000..0faf757f0 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785197903056451700.sql @@ -0,0 +1,161 @@ +-- +DELETE FROM `creature_text` WHERE (`CreatureID` = 24900); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(24900, 0, 0, 'Its power courses through me! I AM A GOD!', 12, 0, 100, 5, 0, 0, 23977, 0, 'Abdul the Insane'), +(24900, 0, 1, 'ABDUL IS STRONG LIKE BULL!', 12, 0, 100, 5, 0, 0, 23978, 0, 'Abdul the Insane'), +(24900, 0, 2, 'ARE YOU CHALLENGING ME? I HAVE THE STRENGTH OF TEN MEN!', 12, 0, 100, 5, 0, 0, 23979, 0, 'Abdul the Insane'); + +DELETE FROM `creature_template_addon` WHERE (`entry` = 24900); +INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(24900, 0, 0, 0, 1, 0, 0, '42459'); + +DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 24902); +INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES +(24902, 0, 0, 1, 0, 0, 0, 0); + +SET @CGUID := 131255; + +DELETE FROM `creature` WHERE `guid` BETWEEN @CGUID+0 AND @CGUID+60 AND `id` IN (24900, 24678, 24676, 24902); +INSERT INTO `creature` (`guid`, `id`, `map`, `zoneId`, `areaId`, `equipment_id`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecs`, `CreateObject`, `VerifiedBuild`) VALUES +-- Abdul the Insane +(@CGUID+0 , 24900, 571, 495, 4226, 1, 538.5657958984375, -3534.0439453125, 13.27492332458496093, 2.76789093017578125, 300, 2, 68887), +-- ID - 29266 Permanent Feign Death +-- Tuskarrs already added +-- (@CGUID+1 , 24678, 571, 495, 4226, 1, 513.756103515625, -3475.178955078125, 3.054831266403198242, 4.328416347503662109, 300, 2, 68887), +-- (@CGUID+2 , 24678, 571, 495, 4226, 1, 546.94964599609375, -3499.382080078125, 1.653660416603088378, 3.996803998947143554, 300, 2, 68887), +-- (@CGUID+3 , 24678, 571, 495, 4226, 1, 550.87042236328125, -3473.961669921875, 6.492102622985839843, 1.082104086875915527, 300, 2, 68887), +-- (@CGUID+4 , 24678, 571, 495, 4226, 1, 599.42919921875, -3485.27392578125, 3.573660135269165039, 1.396263360977172851, 300, 2, 68887), +-- (@CGUID+5 , 24678, 571, 495, 4226, 1, 575.3487548828125, -3477.45263671875, 6.197434425354003906, 1.169370532035827636, 300, 2, 68887), +-- (@CGUID+6 , 24678, 571, 495, 4226, 1, 563.58868408203125, -3447.75439453125, 12.59216117858886718, 3.892084121704101562, 300, 2, 68887), +-- (@CGUID+7 , 24678, 571, 495, 4226, 1, 531.68231201171875, -3453.604248046875, 11.97931098937988281, 1.832595705986022949, 300, 2, 68887), +(@CGUID+8 , 24676, 571, 495, 4226, 1, 534.32720947265625, -3539.286376953125, 13.35808563232421875, 2.111848354339599609, 300, 2, 68887), +(@CGUID+9 , 24676, 571, 495, 4226, 1, 504.918731689453125, -3533.240478515625, 0.43048134446144104, 0.977384388446807861, 300, 2, 68887), +(@CGUID+10, 24676, 571, 495, 4226, 1, 532.400390625, -3535.951904296875, 0.452539354562759399, 1.256637096405029296, 300, 2, 68887), +(@CGUID+11, 24676, 571, 495, 4226, 1, 514.6165771484375, -3537.1767578125, 13.05911636352539062, 5.305800914764404296, 300, 2, 68887), +(@CGUID+12, 24676, 571, 495, 4226, 1, 520.39453125, -3528.095458984375, 12.97960662841796875, 6.248278617858886718, 300, 2, 68887), +(@CGUID+13, 24676, 571, 495, 4226, 1, 507.170318603515625, -3528.2158203125, 0.452536344528198242, 4.764749050140380859, 300, 2, 68887), +(@CGUID+14, 24676, 571, 495, 4226, 1, 502.215240478515625, -3536.001953125, 7.773830413818359375, 5.567600250244140625, 300, 2, 68887), +(@CGUID+15, 24676, 571, 495, 4226, 1, 498.7921142578125, -3526.788330078125, 18.84368896484375, 5.951572895050048828, 300, 2, 68887), +(@CGUID+16, 24676, 571, 495, 4226, 1, 524.718017578125, -3538.3740234375, 13.1880807876586914, 0.506145477294921875, 300, 2, 68887), +(@CGUID+17, 24676, 571, 495, 4226, 1, 518.16339111328125, -3538.41455078125, 7.774502277374267578, 6.230825424194335937, 300, 2, 68887), +(@CGUID+18, 24676, 571, 495, 4226, 1, 515.48565673828125, -3536.5859375, 0.45254334807395935, 1.692969322204589843, 300, 2, 68887), +(@CGUID+19, 24676, 571, 495, 4226, 1, 547.3468017578125, -3542.5078125, 4.519763469696044921, 0.977384388446807861, 300, 2, 68887), +(@CGUID+20, 24676, 571, 495, 4226, 1, 533.98565673828125, -3531.822509765625, 13.15357398986816406, 4.834561824798583984, 300, 2, 68887), +(@CGUID+21, 24676, 571, 495, 4226, 1, 520.6046142578125, -3529.746337890625, 7.774504661560058593, 3.351032257080078125, 300, 2, 68887), +(@CGUID+22, 24676, 571, 495, 4226, 1, 507.74859619140625, -3525.21142578125, 7.773883342742919921, 6.021385669708251953, 300, 2, 68887), +(@CGUID+23, 24676, 571, 495, 4226, 1, 499.4669189453125, -3529.2509765625, 12.82720661163330078, 3.490658521652221679, 300, 2, 68887), +(@CGUID+24, 24676, 571, 495, 4226, 1, 503.6337890625, -3530.083251953125, 18.15540885925292968, 3.874630928039550781, 300, 2, 68887), +(@CGUID+25, 24676, 571, 495, 4226, 1, 531.33056640625, -3540.401611328125, 7.774502277374267578, 4.328416347503662109, 300, 2, 68887), +(@CGUID+26, 24676, 571, 495, 4226, 1, 490.982635498046875, -3527.24658203125, 7.772498607635498046, 4.729842185974121093, 300, 2, 68887), +-- Relays +(@CGUID+27, 24902, 571, 495, 4226, 0, 502.78961181640625, -3529.977783203125, 1.876146316528320312, 5.8817596435546875, 300, 2, 68887), +(@CGUID+28, 24902, 571, 495, 4226, 0, 535.84027099609375, -3536.106689453125, 2.473374366760253906, 0.575958669185638427, 300, 2, 68887), +(@CGUID+29, 24902, 571, 495, 4226, 0, 527.89703369140625, -3537.730224609375, 14.98894596099853515, 4.101523876190185546, 300, 2, 68887), +(@CGUID+30, 24902, 571, 495, 4226, 0, 542.07421875, -3540.149658203125, 25.2296142578125, 1.65806281566619873, 300, 2, 68887), +(@CGUID+31, 24902, 571, 495, 4226, 0, 503.9805908203125, -3535.368408203125, 15.09825325012207031, 1.413716673851013183, 300, 2, 68887), +(@CGUID+32, 24902, 571, 495, 4226, 0, 510.803924560546875, -3532.58935546875, 28.18550300598144531, 0.314159274101257324, 300, 2, 68887), +(@CGUID+33, 24902, 571, 495, 4226, 0, 533.3746337890625, -3540.956298828125, 9.142557144165039062, 3.909537553787231445, 300, 2, 68887), +(@CGUID+34, 24902, 571, 495, 4226, 0, 512.82012939453125, -3538.3798828125, 14.82322120666503906, 3.944444179534912109, 300, 2, 68887), +(@CGUID+35, 24902, 571, 495, 4226, 0, 539.58770751953125, -3539.770751953125, 34.4637908935546875, 4.852015495300292968, 300, 2, 68887), +(@CGUID+36, 24902, 571, 495, 4226, 0, 544.17913818359375, -3536.342041015625, 18.80006599426269531, 0.680678427219390869, 300, 2, 68887), +(@CGUID+37, 24902, 571, 495, 4226, 0, 514.40899658203125, -3533.298095703125, 20.05545234680175781, 5.969026088714599609, 300, 2, 68887), +(@CGUID+38, 24902, 571, 495, 4226, 0, 526.3057861328125, -3531.612060546875, 9.31069183349609375, 3.385938644409179687, 300, 2, 68887), +(@CGUID+39, 24902, 571, 495, 4226, 0, 514.2957763671875, -3521.106689453125, 29.82574272155761718, 4.904375076293945312, 300, 2, 68887), +(@CGUID+40, 24902, 571, 495, 4226, 0, 512.51300048828125, -3522.553955078125, 40.32722854614257812, 5.724679946899414062, 300, 2, 68887), +(@CGUID+41, 24902, 571, 495, 4226, 0, 507.89984130859375, -3537.92041015625, 37.27643585205078125, 5.462880611419677734, 300, 2, 68887), +(@CGUID+42, 24902, 571, 495, 4226, 0, 507.966461181640625, -3525.26171875, 9.635132789611816406, 0.261799395084381103, 300, 2, 68887), +(@CGUID+43, 24902, 571, 495, 4226, 0, 518.94781494140625, -3528.173583984375, 2.695600271224975585, 0.506145477294921875, 300, 2, 68887), +(@CGUID+44, 24902, 571, 495, 4226, 0, 507.234375, -3540.390380859375, 17.92135429382324218, 1.151917338371276855, 300, 2, 68887), +(@CGUID+45, 24902, 571, 495, 4226, 0, 547.06927490234375, -3542.5791015625, 6.49201059341430664, 6.2657318115234375, 300, 2, 68887), +(@CGUID+46, 24902, 571, 495, 4226, 0, 540.0841064453125, -3546.8876953125, 18.93019866943359375, 4.677482128143310546, 300, 2, 68887), +(@CGUID+47, 24902, 571, 495, 4226, 0, 544.32098388671875, -3534.352783203125, 26.10087966918945312, 5.689773082733154296, 300, 2, 68887), +-- Live Crazed Slavers +(@CGUID+48, 24676, 571, 495, 4226, 1, 584.24462890625, -3444.849609375, 15.61687374114990234, 4.455326557159423828, 300, 2, 68887), +(@CGUID+49, 24676, 571, 495, 4226, 1, 603.7803955078125, -3474.509521484375, 5.77075958251953125, 2.223934412002563476, 300, 2, 68887), +(@CGUID+50, 24676, 571, 495, 4226, 1, 528.7786865234375, -3464.49462890625, 7.305242538452148437, 2.169778823852539062, 300, 2, 68887), +(@CGUID+51, 24676, 571, 495, 4226, 1, 542.29071044921875, -3448.540283203125, 12.66628646850585937, 5.86032724380493164, 300, 2, 68887), +(@CGUID+52, 24676, 571, 495, 4226, 1, 511.39642333984375, -3439.80419921875, 11.89968109130859375, 4.935037612915039062, 300, 2, 68887), +(@CGUID+53, 24676, 571, 495, 4226, 1, 608.0699462890625, -3455.2109375, 13.95431327819824218, 5.57839202880859375, 300, 2, 68887), +(@CGUID+54, 24676, 571, 495, 4226, 1, 511.60784912109375, -3464.18994140625, 6.321629524230957031, 4.948744773864746093, 300, 2, 68887), +(@CGUID+55, 24676, 571, 495, 4226, 1, 556.938232421875, -3454.048583984375, 11.112030029296875, 2.213882684707641601, 300, 2, 68887), +(@CGUID+56, 24676, 571, 495, 4226, 1, 540.74676513671875, -3481.5888671875, 4.12340545654296875, 3.639571905136108398, 300, 2, 68887), +(@CGUID+57, 24676, 571, 495, 4226, 1, 536.22503662109375, -3424.553955078125, 15.90343570709228515, 0.815584659576416015, 300, 2, 68887), +(@CGUID+58, 24676, 571, 495, 4226, 1, 552.94879150390625, -3432.64306640625, 16.24846076965332031, 6.042760372161865234, 300, 2, 68887), +(@CGUID+59, 24676, 571, 495, 4226, 1, 566.82171630859375, -3497.459228515625, 3.745334148406982421, 0.587951838970184326, 300, 2, 68887), +(@CGUID+60, 24676, 571, 495, 4226, 1, 563.77935791015625, -3419.53369140625, 17.73925971984863281, 0.82975548505783081, 300, 2, 68887); + +DELETE FROM `creature_addon` WHERE `guid` BETWEEN @CGUID+0 AND @CGUID+60; +INSERT INTO `creature_addon` (`guid`, `path_id`, `bytes2`, `auras`) VALUES +(@CGUID+0 , (@CGUID+0)*10, 1, '42459'), +-- (@CGUID+1 , 0, 1, '29266'), +-- (@CGUID+2 , 0, 1, '29266'), +-- (@CGUID+3 , 0, 1, '29266'), +-- (@CGUID+4 , 0, 1, '29266'), +-- (@CGUID+5 , 0, 1, '29266'), +-- (@CGUID+6 , 0, 1, '29266'), +-- (@CGUID+7 , 0, 1, '29266'), +(@CGUID+8 , 0, 1, '29266'), +(@CGUID+9 , 0, 1, '29266'), +(@CGUID+10, 0, 1, '29266'), +(@CGUID+11, 0, 1, '29266'), +(@CGUID+12, 0, 1, '29266'), +(@CGUID+13, 0, 1, '29266'), +(@CGUID+14, 0, 1, '29266'), +(@CGUID+15, 0, 1, '29266'), +(@CGUID+16, 0, 1, '29266'), +(@CGUID+17, 0, 1, '29266'), +(@CGUID+18, 0, 1, '29266'), +(@CGUID+19, 0, 1, '29266'), +(@CGUID+20, 0, 1, '29266'), +(@CGUID+21, 0, 1, '29266'), +(@CGUID+22, 0, 1, '29266'), +(@CGUID+23, 0, 1, '29266'), +(@CGUID+24, 0, 1, '29266'), +(@CGUID+25, 0, 1, '29266'), +(@CGUID+26, 0, 1, '29266'); + +UPDATE `creature` SET `MovementType` = 2 WHERE `guid` = @CGUID+0 AND `id` = 24900; +DELETE FROM `waypoint_data` WHERE `id` = (@CGUID+0)*10; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`) VALUES +((@CGUID+0)*10, 1 , 520.78326, -3528.5442, 12.9033985, NULL), +((@CGUID+0)*10, 2 , 520.56726, -3528.5063, 12.90191, NULL), +((@CGUID+0)*10, 3 , 518.361, -3528.121, 12.875548, NULL), +((@CGUID+0)*10, 4 , 515.5044, -3527.6223, 12.961787, NULL), +((@CGUID+0)*10, 5 , 515.0117, -3528.3594, 12.976471, NULL), +((@CGUID+0)*10, 6 , 511.1382, -3527.1458, 17.459843, NULL), +((@CGUID+0)*10, 7 , 508.55902, -3526.4094, 17.707817, NULL), +((@CGUID+0)*10, 8 , 493.4898, -3522.895, 19.701223, NULL), +((@CGUID+0)*10, 9 , 491.23422, -3526.7734, 19.972063, NULL), +((@CGUID+0)*10, 10, 491.6969, -3530.882, 19.567657, NULL), +((@CGUID+0)*10, 11, 506.23975, -3536.302, 17.600271, NULL), +((@CGUID+0)*10, 12, 508.6644, -3526.426, 17.695713, NULL), +((@CGUID+0)*10, 13, 515.59094, -3527.4902, 12.962172, NULL), +((@CGUID+0)*10, 14, 516.2962, -3534.7651, 13.047868, NULL), +((@CGUID+0)*10, 15, 515.4622, -3539.2668, 12.90454, NULL), +((@CGUID+0)*10, 16, 525.41144, -3542.12, 13.014915, NULL), +((@CGUID+0)*10, 17, 534.9085, -3545.4343, 13.161723, NULL), +((@CGUID+0)*10, 18, 538.82996, -3540.1965, 13.375676, NULL), +((@CGUID+0)*10, 19, 536.6614, -3533.3171, 13.121769, NULL), +((@CGUID+0)*10, 20, 529.2123, -3530.3767, 12.970497, NULL); + +UPDATE `creature` SET `MovementType` = 1, `wander_distance` = 10 WHERE `guid` BETWEEN @CGUID+48 AND @CGUID+60 AND `id` = 24676; + +-- One Relay casts every 5s +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE (`entry` = 24902); +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = -(@CGUID+27)); +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 +(-(@CGUID+27), 0, 0, 0, 60, 0, 100, 0, 5000, 5000, 5000, 5000, 0, 0, 11, 44886, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Abdul Relay - On Update - Cast \'Storm\'s Fury Staff\''); + +-- Spell only affects other Relays +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 44886) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 24902) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 44886, 0, 0, 31, 0, 3, 24902, 0, 0, 0, 0, '', 'Spell Storm\'s Fury Staff (44886) can only target Abdul Relay (24902)'); + +-- Abdul talks every now and then. Around 20s +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 24900; +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 24900); +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 +(24900, 0, 0, 0, 1, 0, 100, 0, 15000, 30000, 15000, 30000, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Abdul the Insane - Out of Combat - Say Line'); + +-- Delete old Slavers +DELETE FROM `creature` WHERE `guid` IN (104895,104897,104902,104903,104908,104909,104911,104912,104913,104915,104916) AND `id` = 24676; From 08b78d84da594d920d3ed494e5db435a089ac0ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 16:19:55 +0000 Subject: [PATCH 47/69] chore(DB): import pending files Referenced commit(s): 685fb01f0403bfbe4dc3b9ccb7faa45a2def9b51 --- .../rev_1785193948968943800.sql => db_world/2026_07_28_00.sql} | 1 + .../rev_1785197903056451700.sql => db_world/2026_07_28_01.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1785193948968943800.sql => db_world/2026_07_28_00.sql} (98%) rename data/sql/updates/{pending_db_world/rev_1785197903056451700.sql => db_world/2026_07_28_01.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1785193948968943800.sql b/data/sql/updates/db_world/2026_07_28_00.sql similarity index 98% rename from data/sql/updates/pending_db_world/rev_1785193948968943800.sql rename to data/sql/updates/db_world/2026_07_28_00.sql index 3f4521c92..279ee29eb 100644 --- a/data/sql/updates/pending_db_world/rev_1785193948968943800.sql +++ b/data/sql/updates/db_world/2026_07_28_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_26_02 -> 2026_07_28_00 -- UPDATE `spell_area` SET `spell` = 44018 WHERE `spell` = 44017 AND `area` = 3990 AND `quest_start` = 11504 AND `aura_spell` = 0 AND `racemask` = 0 AND `gender` = 2; diff --git a/data/sql/updates/pending_db_world/rev_1785197903056451700.sql b/data/sql/updates/db_world/2026_07_28_01.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1785197903056451700.sql rename to data/sql/updates/db_world/2026_07_28_01.sql index 0faf757f0..39a2cdb96 100644 --- a/data/sql/updates/pending_db_world/rev_1785197903056451700.sql +++ b/data/sql/updates/db_world/2026_07_28_01.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_00 -> 2026_07_28_01 -- DELETE FROM `creature_text` WHERE (`CreatureID` = 24900); INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES From d990c02891e47d4d08131b71781b4430426bdffe Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:20:38 -0300 Subject: [PATCH 48/69] fix(Scripts/Quest): Use Correct Summon Spells for Webbed Creature in Bloodmyst Isle (#26838) --- .../rev_1785189298403213900.sql | 18 +++++++++ .../scripts/Kalimdor/zone_bloodmyst_isle.cpp | 38 +++++-------------- 2 files changed, 28 insertions(+), 28 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1785189298403213900.sql diff --git a/data/sql/updates/pending_db_world/rev_1785189298403213900.sql b/data/sql/updates/pending_db_world/rev_1785189298403213900.sql new file mode 100644 index 000000000..8e3ae45ae --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785189298403213900.sql @@ -0,0 +1,18 @@ +-- +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 17681; +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 17681); +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 +(17681, 0, 0, 0, 54, 0, 100, 0, 0, 0, 0, 0, 0, 0, 80, 1768100, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Expedition Researcher - On Just Summoned - Run Script'), +(17681, 0, 1, 0, 34, 0, 100, 0, 8, 1, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Expedition Researcher - On Reached Point - Despawn Instant'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 1768100); +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 +(1768100, 9, 0, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Expedition Researcher - Actionlist - Say Line'), +(1768100, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 69, 1, 0, 0, 0, 1, 0, 8, 0, 0, 0, 0, -1592.8748, -10851.978, 57.814125, 0, 'Expedition Researcher - Actionlist - Move To Position'); + +DELETE FROM `creature_text` WHERE (`CreatureID` = 17681); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(17681, 0, 0, 'We\'re free! We\'re free!', 12, 7, 100, 0, 0, 0, 14346, 0, 'Expedition Researcher'), +(17681, 0, 1, 'Woot! Thanks!', 12, 7, 100, 0, 0, 0, 14347, 0, 'Expedition Researcher'), +(17681, 0, 2, 'By the forehead signet of Velen, I am saved!', 12, 7, 100, 0, 0, 0, 14348, 0, 'Expedition Researcher'), +(17681, 0, 3, 'I knew Cornelius wouldn\'t leave us behind!', 12, 7, 100, 0, 0, 0, 14349, 0, 'Expedition Researcher'); diff --git a/src/server/scripts/Kalimdor/zone_bloodmyst_isle.cpp b/src/server/scripts/Kalimdor/zone_bloodmyst_isle.cpp index 192e2a5bf..6a1038f21 100644 --- a/src/server/scripts/Kalimdor/zone_bloodmyst_isle.cpp +++ b/src/server/scripts/Kalimdor/zone_bloodmyst_isle.cpp @@ -23,12 +23,13 @@ ## npc_webbed_creature ######*/ -//possible creatures to be spawned -uint32 const possibleSpawns[32] = {17322, 17661, 17496, 17522, 17340, 17352, 17333, 17524, 17654, 17348, 17339, 17345, 17353, 17336, 17550, 17330, 17701, 17321, 17325, 17320, 17683, 17342, 17715, 17334, 17341, 17338, 17337, 17346, 17344, 17327}; - enum WebbedCreature { - NPC_EXPEDITION_RESEARCHER = 17681 + SPELL_FREE_WEBBED_CREATURE_HOSTILE_START = 30954, + SPELL_FREE_WEBBED_CREATURE_HOSTILE_END = 30963, + SPELL_FREE_WEBBED_CREATURE_RESEARCHER = 31010, + + NPC_EXPEDITION_RESEARCHER = 17681 }; class npc_webbed_creature : public CreatureScript @@ -44,39 +45,20 @@ public: void JustEngagedWith(Unit* /*who*/) override { } - void JustDied(Unit* killer) override + void JustDied(Unit* /*killer*/) override { - uint32 spawnCreatureID = 0; - switch (urand(0, 2)) { case 0: - if (Player* player = killer->ToPlayer()) - { - player->KilledMonsterCredit(NPC_EXPEDITION_RESEARCHER); - } - else if (killer->IsPet()) - { - if (Unit* owner = killer->GetOwner()) - { - if (owner->IsPlayer()) - { - owner->ToPlayer()->KilledMonsterCredit(NPC_EXPEDITION_RESEARCHER); - } - } - } - spawnCreatureID = NPC_EXPEDITION_RESEARCHER; + me->CastSpell(me, SPELL_FREE_WEBBED_CREATURE_RESEARCHER, true); + if (Player* player = me->GetLootRecipient()) + player->RewardPlayerAndGroupAtEvent(NPC_EXPEDITION_RESEARCHER, player); break; case 1: case 2: - spawnCreatureID = possibleSpawns[urand(0, 30)]; + me->CastSpell(me, urand(SPELL_FREE_WEBBED_CREATURE_HOSTILE_START, SPELL_FREE_WEBBED_CREATURE_HOSTILE_END), true); break; } - - if (spawnCreatureID) - { - me->SummonCreature(spawnCreatureID, 0.0f, 0.0f, 0.0f, me->GetOrientation(), TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 60000); - } } }; From f58c266642d9f59e9560118a7953f38dc8655ecc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 16:20:41 +0000 Subject: [PATCH 49/69] chore(DB): import pending files Referenced commit(s): 4d40ffbab7d9c7b477bae2fef00c3a338f32cb5a --- .../rev_1785189298403213900.sql => db_world/2026_07_28_02.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785189298403213900.sql => db_world/2026_07_28_02.sql} (98%) diff --git a/data/sql/updates/pending_db_world/rev_1785189298403213900.sql b/data/sql/updates/db_world/2026_07_28_02.sql similarity index 98% rename from data/sql/updates/pending_db_world/rev_1785189298403213900.sql rename to data/sql/updates/db_world/2026_07_28_02.sql index 8e3ae45ae..6589af70d 100644 --- a/data/sql/updates/pending_db_world/rev_1785189298403213900.sql +++ b/data/sql/updates/db_world/2026_07_28_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_01 -> 2026_07_28_02 -- UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 17681; DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 17681); From 662b67dab1bf495348372ab07cec1613acab6b93 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:22:06 -0300 Subject: [PATCH 50/69] fix(Scripts/Quest): Implement Sorlof's Booty Quest Event (#26810) --- .../rev_1785013957591502300.sql | 135 +++++++++ .../game/Spells/SpellInfoCorrections.cpp | 12 + .../scripts/Northrend/zone_howling_fjord.cpp | 269 ++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785013957591502300.sql diff --git a/data/sql/updates/pending_db_world/rev_1785013957591502300.sql b/data/sql/updates/pending_db_world/rev_1785013957591502300.sql new file mode 100644 index 000000000..0b570475a --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785013957591502300.sql @@ -0,0 +1,135 @@ +-- +-- Quest 11529 "Sorlof's Booty": restore the sniffed cannon chain. +-- +-- Clicking The Big Gun used to cast the damage spell itself, instantly, with the +-- clicker as original caster - so Sorlof could be shelled from anywhere on the map by +-- spamming the gun, and the player tagged him and looted his corpse. Sniffs show the +-- click casts the primer, which starts the player's timed Big Gun Assault, which in +-- turn has the gun fire Cannon Assault at Sorlof. +DELETE FROM `npc_spellclick_spells` WHERE `npc_entry` = 24992; +INSERT INTO `npc_spellclick_spells` (`npc_entry`, `spell_id`, `cast_flags`, `user_type`) VALUES +(24992, 45045, 3, 0); -- The Big Gun - Big Cannon Assault Primer + +-- Resolve the nearby-entry target of Big Gun Assault to the gun the player is manning. +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 13 AND `SourceEntry` = 45013; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 45013, 0, 0, 31, 0, 3, 24992, 0, 0, 0, 0, '', 'Spell Big Gun Assault targets The Big Gun'); + +DELETE FROM `spell_script_names` WHERE `spell_id` IN (45013, 45045); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(45045, 'spell_sorlofs_booty_cannon_primer'), +(45013, 'spell_sorlofs_booty_big_gun_assault'); + +-- +-- Sorlof shadows the Sister Mercy along the shore instead of walking his own loop. +-- +-- The ship's stops carry departure events in TaxiPathNode.dbc (path 778), which +-- MotionTransport::DoEventIfAny hands to her GameObjectAI; the script forwards each one +-- to Sorlof as the path he takes to the matching shore position. +UPDATE `gameobject_template` SET `ScriptName` = 'go_sister_mercy' WHERE `entry` = 187038; -- Sister Mercy + +-- His movement, boulder assault and death are all script-driven now. +UPDATE `creature_template` SET `AIName` = '', `ScriptName` = 'npc_sorlof' WHERE `entry` = 24914; -- Sorlof +DELETE FROM `smart_scripts` WHERE `entryorguid` = 24914 AND `source_type` = 0; + +-- Sniffed spawn position; the AI walks him from here on the ship's first departure. +UPDATE `creature` SET `position_x` = 97.17545, `position_y` = -4024.8862, `position_z` = 1.5117704, `orientation` = 2.606328010559082031, `MovementType` = 0 WHERE `guid` = 103278; -- Sorlof + +-- The boulders land on these: crew triggers dotted over the Sister Mercy's decks and +-- rigging. 44965 has no target cap, so the script picks one of them per throw. +SET @CGUID := 73617; + +-- Crew Triggers for Boulder Targets +DELETE FROM `creature` WHERE `id` = 24973 AND `guid` BETWEEN @CGUID+0 AND @CGUID+26; +INSERT INTO `creature` (`guid`, `id`, `map`, `zoneId`, `areaId`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecs`, `CreateObject`, `VerifiedBuild`) VALUES +(@CGUID+0 , 24973, 594, 495, 4077, -20.6305503845214843, 0.87925499677658081, 23.15439796447753906, 2.234021425247192382, 120, 1, 68887), +(@CGUID+1 , 24973, 594, 495, 4077, -26.7290573120117187, 8.964938163757324218, 16.09914779663085937, 1.326450228691101074, 120, 1, 68887), +(@CGUID+2 , 24973, 594, 495, 4077, -28.3763332366943359, 2.132025003433227539, 19.53076934814453125, 2.897246599197387695, 120, 1, 68887), +(@CGUID+3 , 24973, 594, 495, 4077, -16.0938186645507812, 0.273838013410568237, 43.55142974853515625, 4.485496044158935546, 120, 1, 68887), +(@CGUID+4 , 24973, 594, 495, 4077, -18.2567806243896484, 12.23450279235839843, 13.1617746353149414, 4.15388345718383789, 120, 1, 68887), +(@CGUID+5 , 24973, 594, 495, 4077, -21.46112060546875, -0.04458500072360038, 26.67117500305175781, 1.064650893211364746, 120, 1, 68887), +(@CGUID+6 , 24973, 594, 495, 4077, 1.346712946891784667, 13.24960041046142578, 20.16955184936523437, 1.326450228691101074, 120, 1, 68887), +(@CGUID+7 , 24973, 594, 495, 4077, -10.768381118774414, 10.98365497589111328, 11.52657890319824218, 4.590215682983398437, 120, 1, 68887), +(@CGUID+8 , 24973, 594, 495, 4077, 0.120067000389099121, 2.717941045761108398, 9.014368057250976562, 2.827433347702026367, 120, 1, 68887), +(@CGUID+9 , 24973, 594, 495, 4077, -14.4963808059692382, 5.586709022521972656, 19.56297492980957031, 2.652900457382202148, 120, 1, 68887), +(@CGUID+10, 24973, 594, 495, 4077, 2.856342077255249023, 0.252011001110076904, 39.62346649169921875, 2.216568231582641601, 120, 1, 68887), +(@CGUID+11, 24973, 594, 495, 4077, 7.384247779846191406, 4.385578155517578125, 14.35732841491699218, 2.478367567062377929, 120, 1, 68887), +(@CGUID+12, 24973, 594, 495, 4077, -4.44231891632080078, 1.441480040550231933, 30.1070098876953125, 0.104719758033752441, 120, 1, 68887), +(@CGUID+13, 24973, 594, 495, 4077, -0.38327699899673461, 9.437303543090820312, 8.811459541320800781, 4.24114990234375, 120, 1, 68887), +(@CGUID+14, 24973, 594, 495, 4077, -0.61927801370620727, 0.729270994663238525, 15.69870185852050781, 4.712388992309570312, 120, 1, 68887), +(@CGUID+15, 24973, 594, 495, 4077, 2.437870979309082031, 10.67437362670898437, 3.100884199142456054, 5.602506637573242187, 120, 1, 68887), +(@CGUID+16, 24973, 594, 495, 4077, 0.543250977993011474, 0.511954009532928466, 22.23152732849121093, 0.436332315206527709, 120, 1, 68887), +(@CGUID+17, 24973, 594, 495, 4077, 5.890214920043945312, 0.641448020935058593, 56.5892181396484375, 3.228859186172485351, 120, 1, 68887), +(@CGUID+18, 24973, 594, 495, 4077, 2.481215953826904296, -7.58693122863769531, 14.11674880981445312, 2.635447263717651367, 120, 1, 68887), +(@CGUID+19, 24973, 594, 495, 4077, 28.58755874633789062, 7.992822170257568359, 20.73052597045898437, 5.009094715118408203, 120, 1, 68887), +(@CGUID+20, 24973, 594, 495, 4077, 11.99658966064453125, 13.07939910888671875, 10.18214607238769531, 6.230825424194335937, 120, 1, 68887), +(@CGUID+21, 24973, 594, 495, 4077, 21.87375259399414062, 12.49561214447021484, 9.61978912353515625, 3.246312379837036132, 120, 1, 68887), +(@CGUID+22, 24973, 594, 495, 4077, 22.16750526428222656, 1.304870963096618652, 25.48061752319335937, 5.480333805084228515, 120, 1, 68887), +(@CGUID+23, 24973, 594, 495, 4077, 25.33149147033691406, 0.86961299180984497, 46.09729766845703125, 6.213372230529785156, 120, 1, 68887), +(@CGUID+24, 24973, 594, 495, 4077, 32.807464599609375, 3.295629024505615234, 20.36577415466308593, 5.585053443908691406, 120, 1, 68887), +(@CGUID+25, 24973, 594, 495, 4077, 8.728032112121582031, 11.15651130676269531, 3.116890192031860351, 2.39110112190246582, 120, 1, 68887), +(@CGUID+26, 24973, 594, 495, 4077, 20.87360382080078125, 10.74199485778808593, 3.112012147903442382, 4.607669353485107421, 120, 1, 68887); + +-- Boulder Assault: 44965 picks a crew trigger inside its 200 yard reach, lands 44966 on +-- it, and 44967 leaves Creeping Flames burning where it struck. +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 13 AND `SourceEntry` = 44965; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 44965, 0, 0, 31, 0, 3, 24973, 0, 0, 0, 0, '', 'Spell Boulder Assault targets Ellis Crew Trigger'); + +DELETE FROM `spell_script_names` WHERE `spell_id` IN (44965, 44966); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(44965, 'spell_sorlofs_booty_boulder_assault'), +(44966, 'spell_sorlofs_booty_boulder_assault_hit'); + +-- Serverside Boulder Assault: triggers 44965 at the Ellis Crew Trigger every 3s. +DELETE FROM `creature_addon` WHERE `guid` = 103278; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(103278, 0, 0, 0, 1, 0, 3, '44964'); -- Sorlof + +DELETE FROM `waypoint_data` WHERE `id` IN (1032780, 1032781, 1032782, 1032783, 1032784, 1032785); +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`) VALUES +-- Pos 1 (spawn) +(1032780, 1, 97.17545, -4024.8862, 1.5117704, NULL), +-- Pos 2 +(1032781, 1, 101.15443, -3924.839, 2.2858443, NULL), +(1032781, 2, 92.51937, -3893.2766, 1.7101376, NULL), +(1032781, 3, 113.66331, -3885.0088, 3.9941988, NULL), +-- Pos 3 +(1032782, 1, 170.00453, -3834.312, 1.0282993, NULL), +-- Pos 4 +(1032783, 1, 234.34787, -3839.6567, 6.5361958, NULL), +(1032783, 2, 260.00797, -3825.02, 4.976329, NULL), +(1032783, 3, 279.02988, -3805.746, 4.17469, NULL), +(1032783, 4, 266.36356, -3775.4158, 3.9740202, NULL), +-- Pos 5 +(1032784, 1, 307.11368, -3802.3945, 2.1991935, NULL), +(1032784, 2, 327.66074, -3807.0261, 2.548757, NULL), +(1032784, 3, 356.2998, -3801.8398, 1.4324319, NULL), +(1032784, 4, 390.81516, -3765.8672, 0.9260107, NULL), +-- Return path (inferred - he died at the last position) +(1032785, 1, 356.2998, -3801.8398, 1.4324319, NULL), +(1032785, 2, 327.66074, -3807.0261, 2.548757, NULL), +(1032785, 3, 307.11368, -3802.3945, 2.1991935, NULL), +(1032785, 4, 266.36356, -3775.4158, 3.9740202, NULL), +(1032785, 5, 279.02988, -3805.746, 4.17469, NULL), +(1032785, 6, 260.00797, -3825.02, 4.976329, NULL), +(1032785, 7, 234.34787, -3839.6567, 6.5361958, NULL), +(1032785, 8, 170.00453, -3834.312, 1.0282993, NULL), +(1032785, 9, 113.66331, -3885.0088, 3.9941988, NULL), +(1032785, 10, 92.51937, -3893.2766, 1.7101376, NULL), +(1032785, 11, 101.15443, -3924.839, 2.2858443, NULL), +(1032785, 12, 97.17545, -4024.8862, 1.5117704, NULL); + +DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 24992); +INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES +(24992, 0, 0, 1, 1, 0, 0, 0); + +UPDATE `creature_template` SET `flags_extra` = `flags_extra`|128 WHERE (`entry` = 23826); +DELETE FROM `creature_template_addon` WHERE (`entry` = 23826); +INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(23826, 0, 0, 0, 0, 0, 0, '42460'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 24911); +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 +(24911, 0, 0, 1, 60, 0, 100, 0, 3400, 6200, 3400, 6200, 0, 0, 11, 44961, 64, 0, 0, 0, 0, 10, 103278, 24914, 0, 0, 0, 0, 0, 0, 'Cursed Sea Dog - On Update - Cast \'Shoot\' at Sorlof'), +(24911, 0, 1, 0, 61, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Cursed Sea Dog - On Cast \'Shoot\' - Say Line'); diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index eab2da490..9f2347215 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -5242,6 +5242,18 @@ void SpellMgr::LoadSpellInfoCorrections() spellInfo->Effects[EFFECT_0].TargetA = SpellImplicitTargetInfo(TARGET_UNIT_TARGET_ANY); }); + // Boulder Assault (Sorlof's Booty) + ApplySpellFix({ 44966 }, [](SpellInfo* spellInfo) + { + spellInfo->AttributesEx |= SPELL_ATTR1_NO_THREAT; + }); + + // Cannon Assault (Sorlof's Booty) + ApplySpellFix({ 45008 }, [](SpellInfo* spellInfo) + { + spellInfo->AttributesEx3 |= SPELL_ATTR3_ALWAYS_HIT; + }); + for (uint32 i = 0; i < GetSpellInfoStoreSize(); ++i) { SpellInfo* spellInfo = mSpellInfoMap[i]; diff --git a/src/server/scripts/Northrend/zone_howling_fjord.cpp b/src/server/scripts/Northrend/zone_howling_fjord.cpp index 57f698386..98f8ffaee 100644 --- a/src/server/scripts/Northrend/zone_howling_fjord.cpp +++ b/src/server/scripts/Northrend/zone_howling_fjord.cpp @@ -15,13 +15,19 @@ * with this program. If not, see . */ +#include "Containers.h" #include "CreatureScript.h" +#include "GameObjectAI.h" +#include "GameObjectScript.h" +#include "Map.h" +#include "MotionMaster.h" #include "PassiveAI.h" #include "Player.h" #include "ScriptedCreature.h" #include "ScriptedEscortAI.h" #include "SpellInfo.h" #include "SpellScript.h" +#include "WaypointMgr.h" class npc_attracted_reef_bull : public CreatureScript { @@ -417,6 +423,263 @@ class spell_the_cleansing_on_death_cast_on_master : public SpellScript } }; +/*###### +## Quest 11529: Sorlof's Booty +######*/ + +enum SorlofsBooty +{ + NPC_SORLOF = 24914, + NPC_THE_BIG_GUN = 24992, + + SPELL_CANNON_ASSAULT = 45008, + SPELL_SORLOFS_BOOTY = 45070, + + // SPELL_BOULDER_ASSAULT_AURA = 44964, // Serverside, triggers 44965 every 3s + SPELL_BOULDER_ASSAULT_HIT = 44966, + SPELL_BOULDER_ASSAULT_FIRE = 44967, + + // About the maximum range for broadside + CANNON_RANGE = 200, + + // The ship needs to ping Sorlof from wherever she is on her lap + SORLOF_SEARCH_RANGE = 1000, + + SORLOF_WANDER_DISTANCE = 10, + + DATA_SORLOF_TAKE_PATH = 1, + POINT_SORLOF_PATH = 1000, + + // He only knits himself back together once he has broken off and headed home + PATH_SORLOF_RETURN = 1032785, + + // The gun's own SmartAI announces the booty on this data set. + DATA_SORLOF_SLAIN = 1 +}; + +// 44965 - Boulder Assault +class spell_sorlofs_booty_boulder_assault : public SpellScript +{ + PrepareSpellScript(spell_sorlofs_booty_boulder_assault); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_BOULDER_ASSAULT_HIT }); + } + + void FilterTargets(std::list& targets) + { + if (targets.empty()) + return; + + // One boulder per throw from implicit targets (no limit in DBC) + WorldObject* target = Acore::Containers::SelectRandomContainerElement(targets); + targets.clear(); + targets.push_back(target); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + if (Unit* caster = GetCaster()) + caster->CastSpell(GetHitUnit(), SPELL_BOULDER_ASSAULT_HIT, true); + } + + void Register() override + { + OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_sorlofs_booty_boulder_assault::FilterTargets, EFFECT_0, TARGET_UNIT_SRC_AREA_ENTRY); + OnEffectHitTarget += SpellEffectFn(spell_sorlofs_booty_boulder_assault::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 44966 - Boulder Assault +class spell_sorlofs_booty_boulder_assault_hit : public SpellScript +{ + PrepareSpellScript(spell_sorlofs_booty_boulder_assault_hit); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_BOULDER_ASSAULT_FIRE }); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + if (Unit* target = GetHitUnit()) + target->CastSpell(target, SPELL_BOULDER_ASSAULT_FIRE, true); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_sorlofs_booty_boulder_assault_hit::HandleScript, EFFECT_1, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +uint32 GetSorlofPathForShipEvent(uint32 eventId) +{ + switch (eventId) + { + // Departure events of the Sister Mercy's stops (TaxiPathNode.dbc path 778) + case 16501: return 1032780; + case 16502: return 1032781; + case 16503: return 1032782; + case 16504: return 1032783; + case 16510: return 1032784; + case 16511: return 1032785; // Return to spawn + default: return 0; + } +} + +struct npc_sorlof : public ScriptedAI +{ + npc_sorlof(Creature* creature) : ScriptedAI(creature) { } + + void Reset() override + { + _pathId = 0; + _pathNode = 0; + _advancePath = false; + me->SetRegeneratingHealth(false); + } + + void SetData(uint32 id, uint32 value) override + { + if (id != DATA_SORLOF_TAKE_PATH) + return; + + _pathId = value; + _pathNode = 0; + _advancePath = true; + me->SetRegeneratingHealth(value == PATH_SORLOF_RETURN); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE || id != POINT_SORLOF_PATH + _pathNode) + return; + + ++_pathNode; + _advancePath = true; + } + + void JustDied(Unit* /*killer*/) override + { + DoCastSelf(SPELL_SORLOFS_BOOTY, true); + + if (Creature* gun = me->FindNearestCreature(NPC_THE_BIG_GUN, CANNON_RANGE)) + gun->AI()->SetData(DATA_SORLOF_SLAIN, DATA_SORLOF_SLAIN); + } + + void UpdateAI(uint32 /*diff*/) override + { + // Deferred out of MovementInform: that fires from the generator's DoFinalize, and + // MotionMaster::DirectExpire then Resets the new top generator, whose DoReset stops + // the spline it just launched. UpdateAI runs after the MotionMaster, so it sticks. + if (_advancePath) + { + _advancePath = false; + MoveToNextNode(); + } + + if (UpdateVictim()) + DoMeleeAttackIfReady(); + } + +private: + void MoveToNextNode() + { + WaypointPath const* path = sWaypointMgr->GetPath(_pathId); + if (!path) + return; + + // Each leg ends with him milling about where the ship has drawn up + if (_pathNode >= path->Nodes.size()) + { + me->GetMotionMaster()->MoveRandom(SORLOF_WANDER_DISTANCE); + return; + } + + WaypointNode const& node = path->Nodes[_pathNode]; + me->SetWalk(node.MoveType == WAYPOINT_MOVE_TYPE_WALK); + me->GetMotionMaster()->MovePoint(POINT_SORLOF_PATH + _pathNode, node.X, node.Y, node.Z); + } + + uint32 _pathId{ 0 }; + uint32 _pathNode{ 0 }; + bool _advancePath{ false }; +}; + +struct go_sister_mercy : public GameObjectAI +{ + go_sister_mercy(GameObject* go) : GameObjectAI(go) { } + + // Notify Sorlof the ship is departing towards the next broadside point + void EventInform(uint32 eventId) override + { + uint32 pathId = GetSorlofPathForShipEvent(eventId); + if (!pathId) + return; + + if (Creature* sorlof = me->FindNearestCreature(NPC_SORLOF, SORLOF_SEARCH_RANGE)) + sorlof->AI()->SetData(DATA_SORLOF_TAKE_PATH, pathId); + } +}; + +// 45045 - Big Cannon Assault Primer +class spell_sorlofs_booty_cannon_primer : public SpellScript +{ + PrepareSpellScript(spell_sorlofs_booty_cannon_primer); + + bool Validate(SpellInfo const* spellInfo) override + { + return ValidateSpellInfo({ uint32(spellInfo->GetEffect(EFFECT_0).CalcValue()) }); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + // Primes the gun: the clicker performs the actual, timed Big Gun Assault cast. + if (Unit* caster = GetCaster()) + caster->CastSpell(caster, GetSpellInfo()->Effects[EFFECT_0].CalcValue(), false); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_sorlofs_booty_cannon_primer::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 45013 - Big Gun Assault +class spell_sorlofs_booty_big_gun_assault : public SpellScript +{ + PrepareSpellScript(spell_sorlofs_booty_big_gun_assault); + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_CANNON_ASSAULT }); + } + + void HandleDummy(SpellEffIndex /*effIndex*/) + { + // Implicit target of the dummy effect, resolved to The Big Gun by conditions + Creature* gun = GetHitCreature(); + if (!gun) + return; + + Creature* sorlof = gun->FindNearestCreature(NPC_SORLOF, CANNON_RANGE); + if (!sorlof) + return; + + if (!gun->IsWithinLOSInMap(sorlof, VMAP::ModelIgnoreFlags::M2, LINEOFSIGHT_CHECK_VMAP)) + return; + + // Fired by the gun rather than by the player, this is correct + gun->CastSpell(sorlof, SPELL_CANNON_ASSAULT, true); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_sorlofs_booty_big_gun_assault::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); + } +}; + void AddSC_howling_fjord() { new npc_attracted_reef_bull(); @@ -428,4 +691,10 @@ void AddSC_howling_fjord() RegisterSpellScript(spell_the_cleansing_cleansing_soul); RegisterSpellScript(spell_the_cleansing_mirror_image_script_effect); RegisterSpellScript(spell_the_cleansing_on_death_cast_on_master); + RegisterSpellScript(spell_sorlofs_booty_cannon_primer); + RegisterSpellScript(spell_sorlofs_booty_big_gun_assault); + RegisterSpellScript(spell_sorlofs_booty_boulder_assault); + RegisterSpellScript(spell_sorlofs_booty_boulder_assault_hit); + RegisterCreatureAI(npc_sorlof); + RegisterGameObjectAI(go_sister_mercy); } From af97a8724b338ff1de2018b09782fd13ff511e1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 16:23:21 +0000 Subject: [PATCH 51/69] chore(DB): import pending files Referenced commit(s): 662b67dab1bf495348372ab07cec1613acab6b93 --- .../rev_1785013957591502300.sql => db_world/2026_07_28_03.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785013957591502300.sql => db_world/2026_07_28_03.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1785013957591502300.sql b/data/sql/updates/db_world/2026_07_28_03.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1785013957591502300.sql rename to data/sql/updates/db_world/2026_07_28_03.sql index 0b570475a..11582ec18 100644 --- a/data/sql/updates/pending_db_world/rev_1785013957591502300.sql +++ b/data/sql/updates/db_world/2026_07_28_03.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_02 -> 2026_07_28_03 -- -- Quest 11529 "Sorlof's Booty": restore the sniffed cannon chain. -- From dbd8610c32590e41fd90828d569c33286d7cf581 Mon Sep 17 00:00:00 2001 From: sogladev Date: Tue, 28 Jul 2026 18:24:28 +0200 Subject: [PATCH 52/69] fix(Scripts/World): Target Dummy is no longer immortal (#26777) --- src/server/scripts/World/npcs_special.cpp | 35 +++++++++-------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/server/scripts/World/npcs_special.cpp b/src/server/scripts/World/npcs_special.cpp index 94ffc122f..14069c94b 100644 --- a/src/server/scripts/World/npcs_special.cpp +++ b/src/server/scripts/World/npcs_special.cpp @@ -355,42 +355,33 @@ private: std::unordered_map _combatTimer; }; -struct npc_target_dummy : NullCreatureAI +struct npc_target_dummy : ScriptedAI { - npc_target_dummy(Creature* creature) : NullCreatureAI(creature) - { - _deathTimer = 15s; - } + explicit npc_target_dummy(Creature* creature) : ScriptedAI(creature) { } void Reset() override { + scheduler.CancelAll(); + ClearUniqueTimedEventsDone(); + me->SetControlled(true, UNIT_STATE_STUNNED); me->SetLootRecipient(me->GetOwner()); - me->SelectLevel(); - } - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - damage = 0; + ScheduleUniqueTimedEvent(15s, [this] + { + me->SetLootRecipient(me->GetOwner()); // the dummy is lootable by the player who summoned it + me->LowerPlayerDamageReq(me->GetMaxHealth()); + me->KillSelf(); + }, 1); } void UpdateAI(uint32 diff) override { + scheduler.Update(diff); + if (!me->HasUnitState(UNIT_STATE_STUNNED)) me->SetControlled(true, UNIT_STATE_STUNNED); - - _deathTimer -= Milliseconds(diff); - if (_deathTimer <= 0s) - { - me->SetLootRecipient(me->GetOwner()); - me->LowerPlayerDamageReq(me->GetMaxHealth()); - me->KillSelf(); - _deathTimer = 600s; - } } - -private: - Milliseconds _deathTimer; }; /*######## From 0398c6baf804a6ace92b36f3cb2ff3cc76e3e57a Mon Sep 17 00:00:00 2001 From: sogladev Date: Tue, 28 Jul 2026 18:27:47 +0200 Subject: [PATCH 53/69] fix(Scripts/Ulduar): Yogg-Saron reset recovery sequence (#26781) --- .../Ulduar/Ulduar/boss_yoggsaron.cpp | 81 +++++++++++++++---- .../Ulduar/Ulduar/instance_ulduar.cpp | 18 +++++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp index 0ef98e42a..84e8b80cc 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp @@ -174,6 +174,9 @@ enum YoggEvents EVENT_YS_DEAFENING_ROAR = 31, EVENT_YS_SUMMON_GUARDIAN = 32, EVENT_YS_SHADOW_BEACON = 33, + + EVENT_SARA_WIPE_OPEN_DOOR = 40, + EVENT_SARA_WIPE_RESPAWN = 41, }; enum NPCsGOs @@ -257,6 +260,7 @@ enum Misc EVENT_PHASE_ONE = 1, EVENT_PHASE_TWO = 2, EVENT_PHASE_THREE = 3, + EVENT_PHASE_WIPE_RECOVERY = 4, CRITERIA_NOT_GETTING_OLDER = 21001, @@ -388,6 +392,7 @@ struct boss_yoggsaron_sara : public ScriptedAI float _summonSpeed; uint8 _currentIllusion; bool _isIllusionReversed; + bool _isWipeRecovering = false; void AttackStart(Unit*) override { } void MoveInLineOfSight(Unit*) override { } @@ -414,11 +419,10 @@ struct boss_yoggsaron_sara : public ScriptedAI if (!_EnterEvadeMode(why)) return; - Position pos; - pos = me->GetHomePosition(); + Position pos = me->GetHomePosition(); me->NearTeleportTo(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation()); - Reset(); - me->setActive(false); + + HandleWipeRecovery(); } void EnableSara(bool apply) @@ -437,6 +441,18 @@ struct boss_yoggsaron_sara : public ScriptedAI } } + void HandleWipeRecovery() + { + _isWipeRecovering = true; + Reset(); + me->SetVisible(false); + + events.SetPhase(EVENT_PHASE_WIPE_RECOVERY); + events.ScheduleEvent(EVENT_SARA_WIPE_OPEN_DOOR, 20s, 0, EVENT_PHASE_WIPE_RECOVERY); + events.ScheduleEvent(EVENT_SARA_WIPE_RESPAWN, 30s, 0, EVENT_PHASE_WIPE_RECOVERY); + me->setActive(true); + } + void Reset() override { // Whisper only on a real phase 1 wipe, not on the initial reset @@ -455,17 +471,20 @@ struct boss_yoggsaron_sara : public ScriptedAI events.Reset(); summons.DespawnAll(); - me->SetVisible(true); + if (!_isWipeRecovering) + { + me->SetVisible(true); + SpawnClouds(); + UpdateKeeperSpawns(); + } + me->SetDisplayId(me->GetNativeDisplayId()); me->SetDisableGravity(true); me->SetFaction(FACTION_FRIENDLY); me->ClearUnitState(UNIT_STATE_EVADE); EnableSara(false); - SpawnClouds(); _initFight = 1; - - UpdateKeeperSpawns(); _summonedGuardiansCount = 0; _p2TalkTimer = 0; _secondPhase = false; @@ -479,9 +498,12 @@ struct boss_yoggsaron_sara : public ScriptedAI _instance->DoRemoveAurasDueToSpellOnPlayers(SPELL_SANITY); if (Creature* voice = _instance->GetCreature(DATA_VOICE_OF_YOGG_SARON)) voice->AI()->DoAction(ACTION_VOICE_STOP); - _instance->SetBossState(BOSS_YOGGSARON, NOT_STARTED); - if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) - go->SetGoState(GO_STATE_ACTIVE); + if (!_isWipeRecovering) + { + _instance->SetBossState(BOSS_YOGGSARON, NOT_STARTED); + if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) + go->SetGoState(GO_STATE_ACTIVE); + } } } @@ -490,9 +512,6 @@ struct boss_yoggsaron_sara : public ScriptedAI if (!_instance) return; - if (_instance->GetBossState(BOSS_VEZAX) != DONE) - return; - _instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, CRITERIA_NOT_GETTING_OLDER); _instance->SetBossState(BOSS_YOGGSARON, IN_PROGRESS); me->SetInCombatWithZone(); @@ -724,7 +743,6 @@ struct boss_yoggsaron_sara : public ScriptedAI void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType /*damagetype*/, SpellSchoolMask /*damageSchoolMask*/) override { // Guardians can be spawned by walking into Ominous Clouds even when InitFight - // never ran (e.g. Vezax not defeated); their novas must not start phase 2 then. if (!_instance || _instance->GetBossState(BOSS_YOGGSARON) != IN_PROGRESS || !attacker || attacker->GetEntry() != NPC_GUARDIAN_OF_YS || _secondPhase) { damage = 0; @@ -769,6 +787,39 @@ struct boss_yoggsaron_sara : public ScriptedAI void UpdateAI(uint32 diff) override { + if (_isWipeRecovering) + { + events.Update(diff); + while (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_SARA_WIPE_OPEN_DOOR: + if (_instance) + if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) + go->SetGoState(GO_STATE_ACTIVE); + break; + case EVENT_SARA_WIPE_RESPAWN: + if (_instance) + { + if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) + go->SetGoState(GO_STATE_ACTIVE); + + _instance->SetBossState(BOSS_YOGGSARON, NOT_STARTED); + } + + me->SetVisible(true); + SpawnClouds(); + UpdateKeeperSpawns(); + events.Reset(); + _isWipeRecovering = false; + me->setActive(false); + break; + } + } + return; + } + if (_initFight) { _initFight += diff; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 7374ef448..c4afe6229 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -997,6 +997,24 @@ public: } return false; } + + bool CheckRequiredBosses(uint32 bossId, Player const* player) const override + { + if (_SkipCheckRequiredBosses(player)) + return true; + + switch (bossId) + { + case BOSS_YOGGSARON: + if (GetBossState(BOSS_VEZAX) != DONE) + return false; + break; + default: + break; + } + + return true; + } }; }; From b06fc4b8581db91500b8acdd910458aa49d94a2b Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:18:46 -0300 Subject: [PATCH 54/69] fix(Scripts/Ulduar): sync Mimiron's Rocket Strike marker and detonation with the missile (#26844) --- .../rev_1785202717966359900.sql | 5 + .../Northrend/Ulduar/Ulduar/boss_mimiron.cpp | 116 ++++++++++++++++-- 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1785202717966359900.sql diff --git a/data/sql/updates/pending_db_world/rev_1785202717966359900.sql b/data/sql/updates/pending_db_world/rev_1785202717966359900.sql new file mode 100644 index 000000000..64553fc57 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785202717966359900.sql @@ -0,0 +1,5 @@ +-- +DELETE FROM `spell_script_names` WHERE `spell_id` IN (63036, 64064) AND `ScriptName` IN ('spell_mimiron_summon_rocket_strike', 'spell_mimiron_rocket_strike_aura'); +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(63036, 'spell_mimiron_summon_rocket_strike'), +(64064, 'spell_mimiron_rocket_strike_aura'); diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index 92581a93e..2037f393f 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -22,6 +22,7 @@ #include "GameTime.h" #include "GridNotifiers.h" #include "MapMgr.h" +#include "ObjectAccessor.h" #include "PassiveAI.h" #include "Player.h" #include "ScriptedCreature.h" @@ -60,6 +61,7 @@ enum SpellData SPELL_ROCKET_STRIKE_BOTH = 65034, // VX-001 fires both mounted rockets SPELL_ROCKET_STRIKE_TARGET = 63681, // Cast by a fired rocket; picks the impact target (prefers ranged) SPELL_SUMMON_ROCKET_STRIKE = 63036, // Summons the ground strike at the chosen target + SPELL_ROCKET_STRIKE_DAMAGE = 63041, NPC_ROCKET_VISUAL = 34050, NPC_ROCKET_STRIKE_N = 34047, @@ -219,6 +221,9 @@ enum EVENTS EVENT_SUMMON_EMERGENCY_FIRE_BOTS = 68, EVENT_EMERGENCY_BOT_CHECK = 69, EVENT_EMERGENCY_BOT_ATTACK = 70, + + // Rocket (Mimiron Visual): + EVENT_ROCKET_FIRE = 71, }; enum Actions @@ -1877,9 +1882,14 @@ class spell_mimiron_rocket_strike_target_select : public SpellScript void HandleScript(SpellEffIndex /*effIndex*/) { - ObjectGuid originalCaster = GetOriginalCaster() ? GetOriginalCaster()->GetGUID() : GetCaster()->GetGUID(); - GetCaster()->CastSpell(GetHitUnit(), SPELL_SUMMON_ROCKET_STRIKE, TRIGGERED_FULL_MASK, nullptr, nullptr, originalCaster); - GetCaster()->SetDisplayId(11686); // hide the spent rocket until it is reloaded + // Spawn the strike trigger now, so its warning visual and 5s fuse run while the missile is still to come. + // The rocket fires the missile later, timed to land as the fuse expires (see npc_ulduar_mimiron_rocket). + if (Creature* rocket = GetCaster()->ToCreature()) + if (Creature* trigger = rocket->SummonCreature(NPC_ROCKET_STRIKE_N, *GetHitUnit(), TEMPSUMMON_TIMED_DESPAWN, 6000)) + { + rocket->AI()->SetGUID(trigger->GetGUID(), 0); + rocket->AI()->SetGUID(GetHitUnit()->GetGUID(), 1); + } } void Register() override @@ -1906,19 +1916,70 @@ struct npc_ulduar_mimiron_rocket : public NullCreatureAI me->AddUnitState(UNIT_STATE_NO_ENVIRONMENT_UPD); } - void SetData(uint32 /*id*/, uint32 /*value*/) override + void SetGUID(ObjectGuid const& guid, int32 id) override { - me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ() + 100.0f, FORCED_MOVEMENT_NONE, 0.f, false, true); + if (id == 0) + { + _strikeTrigger = guid; + // Delay the shot so the 63036 missile (7 yd/s client-side) lands as the strike trigger's 5s fuse expires. + _travelMs = 0; + if (Creature* trigger = ObjectAccessor::GetCreature(*me, guid)) + _travelMs = uint32(me->GetExactDist(trigger) / 7.0f * 1000.0f); + _events.RescheduleEvent(EVENT_ROCKET_FIRE, Milliseconds(_travelMs < 5000 ? 5000 - _travelMs : 0)); + } + else + _strikeVictim = guid; } - void UpdateAI(uint32 /*diff*/) override + ObjectGuid GetGUID(int32 /*id*/) const override { - if (!me->GetVehicle()) + return _strikeTrigger; + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + if (_events.ExecuteEvent() == EVENT_ROCKET_FIRE) { - me->SetSpeed(MOVE_RUN, me->GetSpeedRate(MOVE_RUN) + 0.4f, false); - me->SetSpeed(MOVE_FLIGHT, me->GetSpeedRate(MOVE_RUN), false); + if (Unit* victim = ObjectAccessor::GetUnit(*me, _strikeVictim)) + me->CastSpell(victim, SPELL_SUMMON_ROCKET_STRIKE, true); + if (Creature* trigger = ObjectAccessor::GetCreature(*me, _strikeTrigger)) + trigger->AI()->SetData(0, _travelMs); + me->SetDisplayId(11686); // hide the spent rocket until it is reloaded } } + +private: + EventMap _events; + ObjectGuid _strikeTrigger; + ObjectGuid _strikeVictim; + uint32 _travelMs = 0; +}; + +// 63036 - Summon Rocket Strike +class spell_mimiron_summon_rocket_strike : public SpellScript +{ + PrepareSpellScript(spell_mimiron_summon_rocket_strike); + + void SetDest(SpellDestination& dest) + { + // Land on the pre-spawned strike trigger, not on the target's current position. + if (Creature* rocket = GetCaster()->ToCreature()) + if (Creature* trigger = ObjectAccessor::GetCreature(*rocket, rocket->AI()->GetGUID())) + dest.Relocate(*trigger); + } + + void PreventSummon(SpellEffIndex effIndex) + { + // The strike trigger is pre-spawned on target selection; this cast only provides the missile visual. + PreventHitDefaultEffect(effIndex); + } + + void Register() override + { + OnDestinationTargetSelect += SpellDestinationTargetSelectFn(spell_mimiron_summon_rocket_strike::SetDest, EFFECT_0, TARGET_DEST_TARGET_ENEMY); + OnEffectHit += SpellEffectFn(spell_mimiron_summon_rocket_strike::PreventSummon, EFFECT_0, SPELL_EFFECT_SUMMON); + } }; struct npc_ulduar_bot_summon_trigger : public NullCreatureAI @@ -2338,11 +2399,24 @@ struct npc_ulduar_rocket_strike_trigger : public NullCreatureAI me->DespawnOrUnsummon(6s); } + void SetData(uint32 /*id*/, uint32 value) override + { + // Detonate in sync with the incoming missile; the 64064 tick is suppressed (spell_mimiron_rocket_strike_aura). + _events.ScheduleEvent(1, Milliseconds(value)); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + if (_events.ExecuteEvent() == 1) + me->CastSpell(me, SPELL_ROCKET_STRIKE_DAMAGE, true); + } + void SpellHitTarget(Unit* target, SpellInfo const* spell) override { if (!target || !spell) return; - if (spell->Id == 63041) + if (spell->Id == SPELL_ROCKET_STRIKE_DAMAGE) { if (target->GetEntry() == NPC_ASSAULT_BOT) me->CastSpell(me, 65040, true); // achievement Not-So-Friendly Fire @@ -2352,6 +2426,26 @@ struct npc_ulduar_rocket_strike_trigger : public NullCreatureAI c->AI()->SetData(0, 13); } } + +private: + EventMap _events; +}; + +// 64064 - Rocket Strike +class spell_mimiron_rocket_strike_aura : public AuraScript +{ + PrepareAuraScript(spell_mimiron_rocket_strike_aura); + + void HandlePeriodic(AuraEffect const* /*aurEff*/) + { + // No fuse tick: the strike trigger detonates in sync with the missile impact (npc_ulduar_rocket_strike_trigger). + PreventDefaultAction(); + } + + void Register() override + { + OnEffectPeriodic += AuraEffectPeriodicFn(spell_mimiron_rocket_strike_aura::HandlePeriodic, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL); + } }; class achievement_mimiron_firefighter : public AchievementCriteriaScript @@ -2415,6 +2509,8 @@ void AddSC_boss_mimiron() RegisterSpellScript(spell_ulduar_mimiron_mine_explosion); RegisterSpellScript(spell_mimiron_rocket_strike); RegisterSpellScript(spell_mimiron_rocket_strike_target_select); + RegisterSpellScript(spell_mimiron_summon_rocket_strike); + RegisterSpellScript(spell_mimiron_rocket_strike_aura); new go_ulduar_do_not_push_this_button(); RegisterUlduarCreatureAI(npc_ulduar_flames_initial); RegisterUlduarCreatureAI(npc_ulduar_flames_spread); From bee797f734431e082ad2b44c4e93a2ff8406bc36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 17:20:15 +0000 Subject: [PATCH 55/69] chore(DB): import pending files Referenced commit(s): b06fc4b8581db91500b8acdd910458aa49d94a2b --- .../rev_1785202717966359900.sql => db_world/2026_07_28_04.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785202717966359900.sql => db_world/2026_07_28_04.sql} (88%) diff --git a/data/sql/updates/pending_db_world/rev_1785202717966359900.sql b/data/sql/updates/db_world/2026_07_28_04.sql similarity index 88% rename from data/sql/updates/pending_db_world/rev_1785202717966359900.sql rename to data/sql/updates/db_world/2026_07_28_04.sql index 64553fc57..c44c6d296 100644 --- a/data/sql/updates/pending_db_world/rev_1785202717966359900.sql +++ b/data/sql/updates/db_world/2026_07_28_04.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_03 -> 2026_07_28_04 -- DELETE FROM `spell_script_names` WHERE `spell_id` IN (63036, 64064) AND `ScriptName` IN ('spell_mimiron_summon_rocket_strike', 'spell_mimiron_rocket_strike_aura'); INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES From 47038fd60b117c8c236d5aceb23b3b0cc5667b18 Mon Sep 17 00:00:00 2001 From: sogladev Date: Wed, 29 Jul 2026 00:23:58 +0200 Subject: [PATCH 56/69] fix(DB/Immunities): Embalming Slime (1) speed and immunity (#26854) Co-authored-by: Gultask <100873791+Gultask@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../updates/pending_db_world/rev_1785070155466962776.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785070155466962776.sql diff --git a/data/sql/updates/pending_db_world/rev_1785070155466962776.sql b/data/sql/updates/pending_db_world/rev_1785070155466962776.sql new file mode 100644 index 000000000..a06c2bfb4 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785070155466962776.sql @@ -0,0 +1,7 @@ +-- +-- Invisibility and Stealth Detection +UPDATE `creature_template_addon` SET `auras` = '18950' WHERE (`entry` IN (16024, 29355)); +-- 3.0 speed +UPDATE `creature_template` SET `speed_run` = 0.42857 WHERE (`entry` = 29355); +-- mech=0x3C8B3E12(DISORIENTED|FEAR|SLEEP|SNARE|STUN|FREEZE|KNOCKOUT|POLYMORPH|BANISH|SHACKLE|HORROR|DAZE|DISCOVERY|IMMUNE_SHIELD|SAPPED) +UPDATE `creature_template` SET `CreatureImmunitiesId` = -393 WHERE (`entry` IN (16024, 29355)); From b8b37e42b03377e86e364839e5241d871ec5c2bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 22:25:07 +0000 Subject: [PATCH 57/69] chore(DB): import pending files Referenced commit(s): 47038fd60b117c8c236d5aceb23b3b0cc5667b18 --- .../rev_1785070155466962776.sql => db_world/2026_07_28_05.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785070155466962776.sql => db_world/2026_07_28_05.sql} (91%) diff --git a/data/sql/updates/pending_db_world/rev_1785070155466962776.sql b/data/sql/updates/db_world/2026_07_28_05.sql similarity index 91% rename from data/sql/updates/pending_db_world/rev_1785070155466962776.sql rename to data/sql/updates/db_world/2026_07_28_05.sql index a06c2bfb4..6d5e75ee0 100644 --- a/data/sql/updates/pending_db_world/rev_1785070155466962776.sql +++ b/data/sql/updates/db_world/2026_07_28_05.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_04 -> 2026_07_28_05 -- -- Invisibility and Stealth Detection UPDATE `creature_template_addon` SET `auras` = '18950' WHERE (`entry` IN (16024, 29355)); From ea1f8110f3ed553ccad725312e2c83a4f684cc53 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Wed, 29 Jul 2026 05:00:48 -0300 Subject: [PATCH 58/69] fix(DB/Quest): Rewrite Trail of Fire in SmartAI (#26856) --- .../rev_1785275627405908300.sql | 131 ++++++++++++++++++ .../scripts/Northrend/zone_howling_fjord.cpp | 118 ---------------- 2 files changed, 131 insertions(+), 118 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1785275627405908300.sql diff --git a/data/sql/updates/pending_db_world/rev_1785275627405908300.sql b/data/sql/updates/pending_db_world/rev_1785275627405908300.sql new file mode 100644 index 000000000..ede60812c --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785275627405908300.sql @@ -0,0 +1,131 @@ +-- +UPDATE `creature_template` SET `ScriptName` = '' WHERE (`entry` = 23784); + +DELETE FROM `script_waypoint` WHERE `entry` = 23784; + +DELETE FROM `creature_template_addon` WHERE (`entry` = 23784); +INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(23784, 0, 0, 0, 1, 0, 0, '5680'); + +SET @CGUID := 117903; +SET @PATH := @CGUID * 10; +DELETE FROM `waypoint_data` WHERE `id`= @PATH; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `delay`, `move_type`) VALUES +(@PATH, 1 , 1379.813, -6409.201, 1.539683, NULL, 0, 1), +(@PATH, 2 , 1380.114, -6401.745, 2.517954, NULL, 0, 1), +(@PATH, 3 , 1380.114, -6401.745, 2.517954, 1.523505, 11000, 1), +(@PATH, 4 , 1394.768, -6387.708, 3.68069, NULL, 0, 1), +(@PATH, 5 , 1405.694, -6380.924, 4.62476, NULL, 0, 1), +(@PATH, 6 , 1416.18, -6370.656, 5.950443, NULL, 0, 1), +(@PATH, 7 , 1425.048, -6361.604, 6.354269, NULL, 0, 1), +(@PATH, 8 , 1427.955, -6350.528, 6.354269, NULL, 0, 1), +(@PATH, 9 , 1424.617, -6340.581, 5.786398, NULL, 0, 1), +(@PATH, 10, 1416.344, -6335.909, 5.705465, NULL, 0, 1), +(@PATH, 11, 1404.921, -6335.286, 6.229269, NULL, 0, 1), +(@PATH, 12, 1404.921, -6335.286, 6.229269, NULL, 0, 1), +(@PATH, 13, 1400.859, -6340.079, 6.476339, NULL, 0, 1), +(@PATH, 14, 1400.859, -6340.079, 6.476339, 3.994878, 9400, 1), +(@PATH, 15, 1419.282, -6332.309, 5.531731, NULL, 0, 1), +(@PATH, 16, 1428.454, -6335.082, 5.604269, NULL, 0, 1), +(@PATH, 17, 1446.955, -6338.777, 7.95246, NULL, 0, 1), +(@PATH, 18, 1457.27, -6343.141, 8.380317, NULL, 0, 1), +(@PATH, 19, 1465.724, -6345.168, 7.788642, NULL, 0, 1), +(@PATH, 20, 1465.724, -6345.168, 7.788642, 6.044203, 1600, 1), +(@PATH, 21, 1471.415, -6347.916, 7.629934, NULL, 5000, 1), +(@PATH, 22, 1461.322, -6337.461, 7.834174, NULL, 0, 1), +(@PATH, 23, 1465.077, -6331.357, 7.562849, NULL, 0, 1), +(@PATH, 24, 1475.621, -6327.219, 7.119575, NULL, 0, 1), +(@PATH, 25, 1490.551, -6315.318, 8.244208, NULL, 0, 1), +(@PATH, 26, 1497.682, -6311.022, 7.41413, NULL, 0, 1), +(@PATH, 27, 1506.431, -6317.528, 7.372158, NULL, 10600, 1), +(@PATH, 28, 1513.499, -6287.491, 5.818699, NULL, 0, 1), +(@PATH, 29, 1513.937, -6277.724, 5.583104, NULL, 0, 1), +(@PATH, 30, 1523.013, -6259.255, 4.599159, NULL, 0, 1), +(@PATH, 31, 1538.941, -6220.414, 6.434206, NULL, 0, 1), +(@PATH, 32, 1554.772, -6204.828, 6.688478, NULL, 0, 1), +(@PATH, 33, 1566.746, -6192.293, 7.589974, NULL, 0, 1), +(@PATH, 34, 1583.782, -6168.592, 8.31788, NULL, 0, 1), +(@PATH, 35, 1588.554, -6163.325, 7.81916, NULL, 0, 1), +(@PATH, 36, 1600.514, -6157.163, 8.667747, NULL, 0, 1), +(@PATH, 37, 1615.063, -6157.817, 9.339622, NULL, 0, 1); + +DELETE FROM `creature_text` WHERE (`CreatureID` = 23784) AND (`GroupID` IN (1, 5)); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(23784, 1, 0, 'Yes, let us leave... but not before we leave our Alliance hosts something to remember us by!', 12, 1, 100, 16, 0, 0, 22486, 0, 'Apothecary Hanes'), +(23784, 5, 0, 'That\'ll teach you to mess with an apothecary, you motherless Alliance dogs!', 14, 1, 100, 14, 0, 0, 22491, 0, 'Apothecary Hanes'); + +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 23784; +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 23784); +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 +(23784, 0, 0 , 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 80, 2378400, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Respawn - Run Script'), +(23784, 0, 1 , 0, 1, 0, 100, 0, 30000, 30000, 30000, 30000, 0, 0, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Out of Combat - Play Emote OneShotBeg if Near Lieutenant Celeyne'), +(23784, 0, 2 , 0, 2, 0, 100, 0, 0, 75, 10000, 10000, 0, 0, 11, 17534, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Between 0-75% Health - Cast \'Healing Potion\''), +(23784, 0, 3 , 0, 19, 0, 100, 0, 11241, 0, 0, 0, 0, 0, 80, 2378401, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Quest \'Trail of Fire\' Taken - Run Script'), +(23784, 0, 4 , 0, 108, 0, 100, 0, 3, 1179030, 0, 0, 0, 0, 80, 2378402, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 3 Reached - Run Script'), +(23784, 0, 5 , 0, 108, 0, 100, 0, 14, 1179030, 0, 0, 0, 0, 80, 2378403, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 14 Reached - Run Script'), +(23784, 0, 6 , 0, 108, 0, 100, 0, 20, 1179030, 0, 0, 0, 0, 80, 2378404, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 20 Reached - Run Script'), +(23784, 0, 7 , 0, 108, 0, 100, 0, 21, 1179030, 0, 0, 0, 0, 80, 2378405, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 21 Reached - Run Script'), +(23784, 0, 8 , 0, 108, 0, 100, 0, 27, 1179030, 0, 0, 0, 0, 80, 2378406, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 27 Reached - Run Script'), +(23784, 0, 9 , 0, 108, 0, 100, 0, 34, 1179030, 0, 0, 0, 0, 1, 7, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 34 Reached - Say Line 7'), +(23784, 0, 10, 0, 108, 0, 100, 0, 37, 1179030, 0, 0, 0, 0, 80, 2378407, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Point 37 Reached - Run Script'), +(23784, 0, 11, 0, 6, 0, 100, 0, 0, 0, 0, 0, 0, 0, 6, 11241, 0, 0, 0, 0, 0, 12, 1, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Just Died - Fail Quest \'Trail of Fire\''), +(23784, 0, 12, 0, 19, 0, 100, 0, 11241, 0, 0, 0, 0, 0, 64, 1, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - On Quest \'Trail of Fire\' Taken - Store Targetlist'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378400); +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 +(2378400, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 90, 8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Set Flag Standstate Kneel'), +(2378400, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 82, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Add Npc Flags Questgiver'), +(2378400, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 2, 1933, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Set Faction 1933'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378401); +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 +(2378401, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 91, 8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Remove FlagStandstate Kneel'), +(2378401, 9, 1, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 0'), +(2378401, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 2, 232, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Set Faction 232'), +(2378401, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, 1179030, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Start Path 1179030'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378402); +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 +(2378402, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 1'), +(2378402, 9, 1, 0, 0, 0, 100, 0, 4600, 4600, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 2'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378403); +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 +(2378403, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 11, 42685, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Cast \'Burn\''), +(2378403, 9, 1, 0, 0, 0, 100, 0, 3000, 3000, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 3'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378404); +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 +(2378404, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 11, 42685, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Cast \'Burn\''); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378405); +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 +(2378405, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 11, 42685, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Cast \'Burn\''), +(2378405, 9, 1, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 4'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378406); +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 +(2378406, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 11, 42685, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Cast \'Burn\''), +(2378406, 9, 1, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 5, 11, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Play Emote 11'), +(2378406, 9, 2, 0, 0, 0, 100, 0, 3000, 3000, 0, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 5'), +(2378406, 9, 3, 0, 0, 0, 100, 0, 6000, 6000, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 6'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 9 AND `entryorguid` = 2378407); +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 +(2378407, 9, 0, 0, 0, 0, 100, 0, 400, 400, 0, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 12, 1, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Set Orientation Stored'), +(2378407, 9, 1, 0, 0, 0, 100, 0, 1200, 1200, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Say Line 8'), +(2378407, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 26, 11241, 0, 0, 0, 0, 0, 12, 1, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Quest Credit \'Trail of Fire\''), +(2378407, 9, 3, 0, 0, 0, 100, 0, 3000, 3000, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Apothecary Hanes - Actionlist - Despawn Instant'); + +UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 23968; +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 23968); +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 +(23968, 0, 0, 0, 8, 0, 100, 0, 42685, 0, 0, 0, 0, 0, 50, 182071, 60000, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Hanes Fire Trigger - On Spellhit \'Burn\' - Summon Gameobject \'Small Chapel Fire\''); + +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 42685) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 23968) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 42685, 0, 0, 31, 0, 3, 23968, 0, 0, 0, 0, '', 'Spell \'Burn\' (42685) from \'Trail of Fire\' (11241) Quest Requires Target \'Hanes Fire Trigger\' (23968)'); + +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 2) AND (`SourceEntry` = 23784) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 29) AND (`ConditionTarget` = 1) AND (`ConditionValue1` = 23964) AND (`ConditionValue2` = 5) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(22, 2, 23784, 0, 0, 29, 1, 23964, 5, 0, 0, 0, 0, '', 'Apothecary Hanes (23784) will only emote Beg when near an alive Lieutenant Celeyne (23964)'); diff --git a/src/server/scripts/Northrend/zone_howling_fjord.cpp b/src/server/scripts/Northrend/zone_howling_fjord.cpp index 98f8ffaee..e4e6771d7 100644 --- a/src/server/scripts/Northrend/zone_howling_fjord.cpp +++ b/src/server/scripts/Northrend/zone_howling_fjord.cpp @@ -71,123 +71,6 @@ public: } }; -/*###### -## npc_apothecary_hanes -######*/ -enum Entries -{ - NPC_APOTHECARY_HANES = 23784, - NPC_HANES_FIRE_TRIGGER = 23968, - QUEST_TRAIL_OF_FIRE = 11241, - SPELL_COSMETIC_LOW_POLY_FIRE = 56274, - SPELL_HEALING_POTION = 17534 -}; - -class npc_apothecary_hanes : public CreatureScript -{ -public: - npc_apothecary_hanes() : CreatureScript("npc_apothecary_hanes") { } - - bool OnQuestAccept(Player* player, Creature* creature, Quest const* quest) override - { - if (quest->GetQuestId() == QUEST_TRAIL_OF_FIRE) - { - creature->SetFaction(player->GetTeamId() == TEAM_ALLIANCE ? FACTION_ESCORTEE_A_PASSIVE : FACTION_ESCORTEE_H_PASSIVE); - creature->SetWalk(true); - CAST_AI(npc_escortAI, (creature->AI()))->Start(true, player->GetGUID()); - } - return true; - } - - struct npc_Apothecary_HanesAI : public npc_escortAI - { - npc_Apothecary_HanesAI(Creature* creature) : npc_escortAI(creature) { } - uint32 PotTimer; - - void Reset() override - { - SetDespawnAtFar(false); - PotTimer = 10000; //10 sec cooldown on potion - } - - void JustDied(Unit* /*killer*/) override - { - if (Player* player = GetPlayerForEscort()) - player->FailQuest(QUEST_TRAIL_OF_FIRE); - } - - void UpdateEscortAI(uint32 diff) override - { - if (HealthBelowPct(75)) - { - if (PotTimer <= diff) - { - DoCast(me, SPELL_HEALING_POTION, true); - PotTimer = 10000; - } - else PotTimer -= diff; - } - if (GetAttack() && UpdateVictim()) - DoMeleeAttackIfReady(); - } - - using CreatureAI::WaypointReached; - void WaypointReached(uint32 waypointId) override - { - Player* player = GetPlayerForEscort(); - if (!player) - return; - - switch (waypointId) - { - case 1: - me->SetReactState(REACT_AGGRESSIVE); - me->SetWalk(false); - break; - case 23: - player->GroupEventHappens(QUEST_TRAIL_OF_FIRE, me); - me->DespawnOrUnsummon(); - break; - case 5: - if (Unit* Trigger = me->FindNearestCreature(NPC_HANES_FIRE_TRIGGER, 10.0f)) - Trigger->CastSpell(Trigger, SPELL_COSMETIC_LOW_POLY_FIRE, false); - me->SetWalk(true); - break; - case 6: - if (Unit* Trigger = me->FindNearestCreature(NPC_HANES_FIRE_TRIGGER, 10.0f)) - Trigger->CastSpell(Trigger, SPELL_COSMETIC_LOW_POLY_FIRE, false); - me->SetWalk(false); - break; - case 8: - if (Unit* Trigger = me->FindNearestCreature(NPC_HANES_FIRE_TRIGGER, 10.0f)) - Trigger->CastSpell(Trigger, SPELL_COSMETIC_LOW_POLY_FIRE, false); - me->SetWalk(true); - break; - case 9: - if (Unit* Trigger = me->FindNearestCreature(NPC_HANES_FIRE_TRIGGER, 10.0f)) - Trigger->CastSpell(Trigger, SPELL_COSMETIC_LOW_POLY_FIRE, false); - break; - case 10: - me->SetWalk(false); - break; - case 13: - me->SetWalk(true); - break; - case 14: - if (Unit* Trigger = me->FindNearestCreature(NPC_HANES_FIRE_TRIGGER, 10.0f)) - Trigger->CastSpell(Trigger, SPELL_COSMETIC_LOW_POLY_FIRE, false); - me->SetWalk(false); - break; - } - } - }; - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_Apothecary_HanesAI(creature); - } -}; - /*###### ## npc_plaguehound_tracker ######*/ @@ -683,7 +566,6 @@ class spell_sorlofs_booty_big_gun_assault : public SpellScript void AddSC_howling_fjord() { new npc_attracted_reef_bull(); - new npc_apothecary_hanes(); new npc_plaguehound_tracker(); RegisterCreatureAI(npc_rodin_lightning_enabler); RegisterSpellScript(spell_hawk_hunting); From 40d2b7535be41247712407b869cbed3737cadb47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jul 2026 08:01:58 +0000 Subject: [PATCH 59/69] chore(DB): import pending files Referenced commit(s): ea1f8110f3ed553ccad725312e2c83a4f684cc53 --- .../rev_1785275627405908300.sql => db_world/2026_07_29_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785275627405908300.sql => db_world/2026_07_29_00.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1785275627405908300.sql b/data/sql/updates/db_world/2026_07_29_00.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1785275627405908300.sql rename to data/sql/updates/db_world/2026_07_29_00.sql index ede60812c..01da2b384 100644 --- a/data/sql/updates/pending_db_world/rev_1785275627405908300.sql +++ b/data/sql/updates/db_world/2026_07_29_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_28_05 -> 2026_07_29_00 -- UPDATE `creature_template` SET `ScriptName` = '' WHERE (`entry` = 23784); From 5bdaa898db6180f1121f8364f289447091f1975d Mon Sep 17 00:00:00 2001 From: sogladev Date: Wed, 29 Jul 2026 10:02:16 +0200 Subject: [PATCH 60/69] fix(Scripts/Ulduar): Hodir Flash Freeze NPCs outro sequence (#26855) --- .../Northrend/Ulduar/Ulduar/boss_hodir.cpp | 175 +++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp index f8f406e3b..71ccf0fbc 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp @@ -26,6 +26,7 @@ #include "SpellScript.h" #include "SpellScriptLoader.h" #include "ulduar.h" +#include enum HodirSpellData { @@ -136,6 +137,11 @@ enum HodirEvents EVENT_MAGE_TOASTY_FIRE = 18, EVENT_MAGE_FIREBALL = 19, EVENT_MAGE_MELT_ICE = 20, + + EVENT_VICTORY_CHEER_2 = 21, + EVENT_VICTORY_CHEER_3 = 22, + EVENT_VICTORY_DANCE = 23, + EVENT_VICTORY_DESPAWN = 24, }; enum HodirText @@ -162,6 +168,11 @@ enum HodirSounds SOUND_HODIR_BERSERK = 15558, }; +enum HodirHelperActions +{ + ACTION_VICTORY_EMOTE = 1, +}; + struct HodirHelperData { uint32 id; @@ -342,7 +353,15 @@ struct boss_hodir : public BossAI me->RemoveAllAuras(); events.Reset(); - summons.DespawnAll(); + + // Manually despawn helpers + summons.DespawnIf([this](ObjectGuid guid) -> bool + { + return std::ranges::none_of(Helpers, [guid](ObjectGuid Helper) { return guid == Helper; }); + }); + + // Start victory emote sequence on surviving helpers + DoHelperVictoryEmotes(); Talk(TEXT_DEATH); scheduler.Schedule(14s, [this](TaskContext /*context*/) @@ -518,6 +537,36 @@ struct boss_hodir : public BossAI } } + void DoHelperVictoryEmotes() + { + for (uint8 i = 0; i < 8; ++i) + { + Creature* helper = GetHelper(i); + if (!helper) + continue; + + if (!helper->IsAlive()) + { + helper->DespawnOrUnsummon(); + continue; + } + + // Stop combat behavior and start victory emote chain + helper->AI()->DoAction(ACTION_VICTORY_EMOTE); + + // Thaw if still frozen + if (helper->HasAura(SPELL_FLASH_FREEZE_TRAPPED_NPC)) + { + helper->RemoveAura(SPELL_FLASH_FREEZE_TRAPPED_NPC); + if (Creature* iceBlock = helper->FindNearestCreature(NPC_FLASH_FREEZE_NPC, 5.0f)) + iceBlock->DespawnOrUnsummon(); + } + + // First cheer + helper->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + } + } + void KilledUnit(Unit* who) override { if (who->IsPlayer()) @@ -753,6 +802,37 @@ struct npc_ulduar_hodir_priest : public ScriptedAI me->CastSpell(victim, SPELL_PRIEST_SMITE, false); events.Repeat(2100ms); break; + case EVENT_VICTORY_CHEER_2: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_CHEER_3, 3s, 6500ms); + break; + case EVENT_VICTORY_CHEER_3: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_DANCE, 3s, 6500ms); + break; + case EVENT_VICTORY_DANCE: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_DANCE); + events.ScheduleEvent(EVENT_VICTORY_DESPAWN, 15s, 60s); + break; + case EVENT_VICTORY_DESPAWN: + me->DespawnOrUnsummon(); + break; + } + } + + void DoAction(int32 action) override + { + if (action == ACTION_VICTORY_EMOTE) + { + events.Reset(); + me->AttackStop(); + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + events.ScheduleEvent(EVENT_VICTORY_CHEER_2, 3s, 6500ms); } } @@ -839,6 +919,37 @@ struct npc_ulduar_hodir_druid : public ScriptedAI } events.Repeat(3s); break; + case EVENT_VICTORY_CHEER_2: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_CHEER_3, 3s, 6500ms); + break; + case EVENT_VICTORY_CHEER_3: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_DANCE, 3s, 6500ms); + break; + case EVENT_VICTORY_DANCE: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_DANCE); + events.ScheduleEvent(EVENT_VICTORY_DESPAWN, 15s, 60s); + break; + case EVENT_VICTORY_DESPAWN: + me->DespawnOrUnsummon(); + break; + } + } + + void DoAction(int32 action) override + { + if (action == ACTION_VICTORY_EMOTE) + { + events.Reset(); + me->AttackStop(); + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + events.ScheduleEvent(EVENT_VICTORY_CHEER_2, 3s, 6500ms); } } @@ -932,6 +1043,37 @@ struct npc_ulduar_hodir_shaman : public ScriptedAI events.Repeat(30s); break; } + case EVENT_VICTORY_CHEER_2: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_CHEER_3, 3s, 6500ms); + break; + case EVENT_VICTORY_CHEER_3: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_DANCE, 3s, 6500ms); + break; + case EVENT_VICTORY_DANCE: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_DANCE); + events.ScheduleEvent(EVENT_VICTORY_DESPAWN, 15s, 60s); + break; + case EVENT_VICTORY_DESPAWN: + me->DespawnOrUnsummon(); + break; + } + } + + void DoAction(int32 action) override + { + if (action == ACTION_VICTORY_EMOTE) + { + events.Reset(); + me->AttackStop(); + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + events.ScheduleEvent(EVENT_VICTORY_CHEER_2, 3s, 6500ms); } } @@ -1036,6 +1178,37 @@ struct npc_ulduar_hodir_mage : public ScriptedAI events.Repeat(5s); } break; + case EVENT_VICTORY_CHEER_2: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_CHEER_3, 3s, 6500ms); + break; + case EVENT_VICTORY_CHEER_3: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_CHEER_NO_SHEATHE); + events.ScheduleEvent(EVENT_VICTORY_DANCE, 3s, 6500ms); + break; + case EVENT_VICTORY_DANCE: + if (me->IsAlive()) + me->HandleEmoteCommand(EMOTE_ONESHOT_DANCE); + events.ScheduleEvent(EVENT_VICTORY_DESPAWN, 15s, 60s); + break; + case EVENT_VICTORY_DESPAWN: + me->DespawnOrUnsummon(); + break; + } + } + + void DoAction(int32 action) override + { + if (action == ACTION_VICTORY_EMOTE) + { + events.Reset(); + me->AttackStop(); + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + events.ScheduleEvent(EVENT_VICTORY_CHEER_2, 3s, 6500ms); } } From 7c517859ef99337e13c3777084b7ccf0de711de4 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:08:04 -0300 Subject: [PATCH 61/69] fix(DB/Creature): Improve If Valguarde Falls... with sniffed creature data (#26865) --- .../rev_1785360115487829100.sql | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785360115487829100.sql diff --git a/data/sql/updates/pending_db_world/rev_1785360115487829100.sql b/data/sql/updates/pending_db_world/rev_1785360115487829100.sql new file mode 100644 index 000000000..4892bb47f --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785360115487829100.sql @@ -0,0 +1,103 @@ +-- +DELETE FROM `creature_template_addon` WHERE (`entry` = 24051); +INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(24051, 0, 0, 0, 1, 0, 0, '42865'); + +DELETE FROM `creature_template_addon` WHERE (`entry` = 24063); +INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(24063, 0, 0, 0, 0, 0, 0, '42865'); + +-- Empty equip_template to not throw errors +DELETE FROM `creature_equip_template` WHERE (`CreatureID` = 24063); +INSERT INTO `creature_equip_template` (`CreatureID`, `ID`, `ItemID1`, `ItemID2`, `ItemID3`, `VerifiedBuild`) VALUES +(24063, 1, 0, 0, 0, 68887); + +UPDATE `creature_template` SET `lootid` = 0 WHERE (`entry` = 24063); +DELETE FROM `creature_loot_template` WHERE `Entry` = 24063; + +UPDATE `creature_template` SET `flags_extra` = `flags_extra`|64|134217728, `detection_range` = 20 WHERE (`entry` IN (24051, 24063)); + +UPDATE `creature_template` SET `speed_walk` = 1, `speed_run` = 1.142857 WHERE (`entry` = 24063); + +-- Delete old ones +DELETE FROM `creature` WHERE (`id` = 24063) AND (`guid` BETWEEN 120557 AND 120561); +DELETE FROM `creature` WHERE (`id` = 24051) AND (`guid` BETWEEN 118766 AND 118773); +DELETE FROM `creature_addon` WHERE (`guid` BETWEEN 120557 AND 120561); +DELETE FROM `creature_addon` WHERE (`guid` BETWEEN 118766 AND 118773); +DELETE FROM `waypoint_data` WHERE (`id` BETWEEN 1205570 AND 1205610); +DELETE FROM `waypoint_data` WHERE (`id` BETWEEN 1187660 AND 1187730); + +SET @CGUID := 12804; + +DELETE FROM `creature` WHERE `guid` BETWEEN @CGUID AND @CGUID+6 AND `id` = 24051; +INSERT INTO `creature` (`guid`, `id`, `map`, `zoneId`, `areaId`, `equipment_id`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecs`, `CreateObject`, `VerifiedBuild`) VALUES +(@CGUID+0, 24051, 571, 495, 3981, 1, 733.43774, -4872.8115, 5.9952602, 2.02116, 120, 1, 68887), +(@CGUID+1, 24051, 571, 495, 3981, 1, 811.02466, -4967.3896, 1.8235762, 0.0481025, 120, 1, 68887), +(@CGUID+2, 24051, 571, 495, 3981, 1, 798.0802, -4867.249, -0.5787722, 0.366519, 120, 1, 68887), +(@CGUID+3, 24051, 571, 495, 3981, 1, 780.9059, -4882.871, 0.5644784, 3.03564, 120, 1, 68887), +(@CGUID+4, 24051, 571, 495, 3981, 1, 783.75476, -4949.7236, 1.654979, 3.02413, 120, 1, 68887), +(@CGUID+5, 24051, 571, 495, 3981, 1, 794.9075, -4874.745, -0.62727714, 3.11962, 120, 1, 68887), +(@CGUID+6, 24051, 571, 495, 3981, 1, 803.7593, -4924.593, 0.1089665, 3.11962, 120, 1, 68887); + +DELETE FROM `creature_multispawn` WHERE `spawnId` BETWEEN @CGUID+0 AND @CGUID+6 AND `entry` = 24063; +INSERT INTO `creature_multispawn` (`spawnId`, `entry`) VALUES +(@CGUID+0, 24063), +(@CGUID+1, 24063), +(@CGUID+2, 24063), +(@CGUID+3, 24063), +(@CGUID+4, 24063), +(@CGUID+5, 24063), +(@CGUID+6, 24063); + +DELETE FROM `waypoint_data` WHERE `id` BETWEEN (@CGUID+0)*10 AND (@CGUID+6)*10; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `move_type`) VALUES +-- X: 733.43774 Y: -4872.8115 Z: 5.9952602 +((@CGUID+0)*10, 1, 732.7387, -4884.812, 3.0737715, NULL, 1), +((@CGUID+0)*10, 2, 719.05145, -4894.5073, 6.2719183, NULL, 1), +((@CGUID+0)*10, 3, 697.83594, -4907.74, 6.1251554, NULL, 1), +-- X: 811.02466 Y: -4967.3896 Z: 1.8235762 +((@CGUID+1)*10, 1, 744.038, -4960.3228, 2.330449, NULL, 1), +((@CGUID+1)*10, 2, 727.86505, -4985.579, 5.602709, NULL, 1), +((@CGUID+1)*10, 3, 722.78345, -4988.4126, 6.404078, NULL, 1), +-- X: 798.0802 Y: -4867.249 Z: -0.5787722 +((@CGUID+2)*10, 1, 741.7717, -4904.015, 3.1975787, NULL, 1), +((@CGUID+2)*10, 2, 734.9463, -4927.025, 6.25686, NULL, 1), +((@CGUID+2)*10, 3, 719.4844, -4935.973, 6.7613916, NULL, 1), +-- X: 780.9059 Y: -4882.871 Z: 0.5644784 +((@CGUID+3)*10, 1, 730.73535, -4893.994, 4.5011435, NULL, 1), +((@CGUID+3)*10, 2, 704.7542, -4905.6694, 7.276061, NULL, 1), +((@CGUID+3)*10, 3, 709.357, -4927.3623, 7.2387867, NULL, 1), +-- X: 783.75476 Y: -4949.7236 Z: 1.654979 +((@CGUID+4)*10, 1, 728.9895, -4940.7905, 6.076869, NULL, 1), +((@CGUID+4)*10, 2, 715.9845, -4970.6724, 6.0854883, NULL, 1), +-- X: 794.9075 Y: -4874.745 Z: -0.62727714 +((@CGUID+5)*10, 1, 738.62933, -4915.262, 5.2849817, NULL, 1), +((@CGUID+5)*10, 2, 731.65607, -4927.6084, 6.734372, NULL, 1), +((@CGUID+5)*10, 3, 712.5723, -4926.47, 7.523102, NULL, 1), +-- X: 803.7593 Y: -4924.593 Z: 0.1089665 +((@CGUID+6)*10, 1, 739.2899, -4937.828, 5.6794043, NULL, 1), +((@CGUID+6)*10, 2, 719.42474, -4959.9834, 4.882431, NULL, 1); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 24051); +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 +(24051, 0, 0, 0, 9, 0, 100, 0, 5700, 11700, 5700, 11700, 8, 25, 11, 27577, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - Within 8-25 Range - Cast \'Intercept\''), +(24051, 0, 1, 0, 9, 0, 100, 0, 8700, 14700, 8700, 14700, 10, 25, 11, 42870, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - Within 10-25 Range - Cast \'Throw Dragonflayer Harpoon\''), +(24051, 0, 2, 0, 4, 0, 10, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Aggro - Say Line'), +(24051, 0, 3, 0, 6, 0, 100, 0, 0, 0, 0, 0, 0, 0, 41, 5000, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Just Died - Despawn In 5000 ms'), +(24051, 0, 4, 0, 109, 0, 100, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 19, 23739, 60, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Path Finished - Start Attacking'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 24063); +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 +(24063, 0, 0, 0, 0, 0, 70, 0, 5000, 10000, 5000, 10000, 0, 0, 11, 7367, 32, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Worg - In Combat - Cast \'Infected Bite\''), +(24063, 0, 1, 0, 6, 0, 100, 0, 0, 0, 0, 0, 0, 0, 41, 5000, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Worg - On Just Died - Despawn In 5000 ms'), +(24063, 0, 2, 0, 109, 0, 100, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 19, 23739, 60, 0, 0, 0, 0, 0, 0, 'Dragonflayer Worg - On Path Finished - Start Attacking'); + +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` BETWEEN -(@CGUID+6) AND -(@CGUID+0)); +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(-(@CGUID+0), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+0)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+1), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+1)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+2), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+2)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+3), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+3)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+4), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+4)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+5), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+5)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'), +(-(@CGUID+6), 0, 1000, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, (@CGUID+6)*10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Invader - On Respawn - Start Path'); From 9963e603961dc98607f2e8e20a8cd25f129e91dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Jul 2026 11:09:21 +0000 Subject: [PATCH 62/69] chore(DB): import pending files Referenced commit(s): 7c517859ef99337e13c3777084b7ccf0de711de4 --- .../rev_1785360115487829100.sql => db_world/2026_07_30_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785360115487829100.sql => db_world/2026_07_30_00.sql} (99%) diff --git a/data/sql/updates/pending_db_world/rev_1785360115487829100.sql b/data/sql/updates/db_world/2026_07_30_00.sql similarity index 99% rename from data/sql/updates/pending_db_world/rev_1785360115487829100.sql rename to data/sql/updates/db_world/2026_07_30_00.sql index 4892bb47f..f3ae10646 100644 --- a/data/sql/updates/pending_db_world/rev_1785360115487829100.sql +++ b/data/sql/updates/db_world/2026_07_30_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_29_00 -> 2026_07_30_00 -- DELETE FROM `creature_template_addon` WHERE (`entry` = 24051); INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES From 0c42af162ad99232b7a9f83daf5ab958339e3795 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:01:26 -0300 Subject: [PATCH 63/69] fix(DB/Creature): Use correct model probabilities for Vrykul Harpoon Gun (#26881) --- .../sql/updates/pending_db_world/rev_1785444668294844700.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785444668294844700.sql diff --git a/data/sql/updates/pending_db_world/rev_1785444668294844700.sql b/data/sql/updates/pending_db_world/rev_1785444668294844700.sql new file mode 100644 index 000000000..92916fa61 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785444668294844700.sql @@ -0,0 +1,5 @@ +-- +DELETE FROM `creature_template_model` WHERE (`CreatureID` = 24701); +INSERT INTO `creature_template_model` (`CreatureID`, `Idx`, `CreatureDisplayID`, `DisplayScale`, `Probability`, `VerifiedBuild`) VALUES +(24701, 1, 17188, 1, 0, 51831), +(24701, 0, 22522, 1, 1, 51831); From 3a330de98b52b6d8a6ff0efc00dca2d5aba6e00c Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:02:30 -0300 Subject: [PATCH 64/69] fix(Core/Movement): Prevent SmartAI vehicles from following the player on evades (#26882) --- src/server/game/AI/SmartScripts/SmartAI.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index 560b5624e..8d20fe668 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -759,7 +759,9 @@ void SmartAI::EnterEvadeMode(EvadeReason why) if (Unit* owner = me->GetCharmerOrOwner()) { - me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, me->GetFollowAngle()); + if (!me->IsVehicle()) // vehicles should not follow their owner (passenger) + me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, me->GetFollowAngle()); + me->ClearUnitState(UNIT_STATE_EVADE); } else if (HasEscortState(SMART_ESCORT_ESCORTING)) From adcd88a0b0772634db2fd840af0d2542d11a9143 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 31 Jul 2026 09:02:43 +0000 Subject: [PATCH 65/69] chore(DB): import pending files Referenced commit(s): 0c42af162ad99232b7a9f83daf5ab958339e3795 --- .../rev_1785444668294844700.sql => db_world/2026_07_31_00.sql} | 1 + 1 file changed, 1 insertion(+) rename data/sql/updates/{pending_db_world/rev_1785444668294844700.sql => db_world/2026_07_31_00.sql} (86%) diff --git a/data/sql/updates/pending_db_world/rev_1785444668294844700.sql b/data/sql/updates/db_world/2026_07_31_00.sql similarity index 86% rename from data/sql/updates/pending_db_world/rev_1785444668294844700.sql rename to data/sql/updates/db_world/2026_07_31_00.sql index 92916fa61..9fa17e5ba 100644 --- a/data/sql/updates/pending_db_world/rev_1785444668294844700.sql +++ b/data/sql/updates/db_world/2026_07_31_00.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_30_00 -> 2026_07_31_00 -- DELETE FROM `creature_template_model` WHERE (`CreatureID` = 24701); INSERT INTO `creature_template_model` (`CreatureID`, `Idx`, `CreatureDisplayID`, `DisplayScale`, `Probability`, `VerifiedBuild`) VALUES From 21abfe0bc42ea629d8bfbf32ccd4a05f06822073 Mon Sep 17 00:00:00 2001 From: Gultask <100873791+Gultask@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:05:21 -0300 Subject: [PATCH 66/69] fix(Scripts/HowlingFjord): Harrowmeiser and Petrov Gossips (#26875) --- .../rev_1785371667750135300.sql | 15 ++ src/server/scripts/World/transport_zeppelin.h | 6 + .../scripts/World/transport_zeppelins.cpp | 164 ++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1785371667750135300.sql diff --git a/data/sql/updates/pending_db_world/rev_1785371667750135300.sql b/data/sql/updates/pending_db_world/rev_1785371667750135300.sql new file mode 100644 index 000000000..2a7ddf0b6 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1785371667750135300.sql @@ -0,0 +1,15 @@ +-- +-- Zeppelin, Alliance - Westguard Keep to Shattered Straits (taxi path 727) +UPDATE `gameobject_template` SET `ScriptName` = 'go_transport_westguard_zeppelin' WHERE (`entry` = 186371); + +-- Harrowmeiser - dock announcements, fired from the taxi arrival event 15431 +DELETE FROM `creature_text` WHERE (`CreatureID` = 23823); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(23823, 0, 0, 'My zeppelin has docked. Despite my being held captive, and the sorry state that she\'s in, she\'ll be departing the dock again in two minutes.', 14, 0, 100, 0, 0, 0, 22336, 0, 'Harrowmeiser - Arrival'), +(23823, 1, 0, 'The zeppelin\'s leaving for a tour around the bay in less than one minute! Oh, and if anyone feels like setting me free, I\'m on the dock.', 14, 0, 100, 0, 0, 0, 22337, 0, 'Harrowmeiser - Departure'); + +-- Harrowmeiser and Bombardier Petrov - gossip text picked from the zeppelin's path +-- progress, which also pushes the world state that npc_text 11322 and 11332 substitute +-- into "in less than $3078w minutes" +UPDATE `creature_template` SET `ScriptName` = 'npc_harrowmeiser' WHERE (`entry` = 23823); +UPDATE `creature_template` SET `ScriptName` = 'npc_bombardier_petrov' WHERE (`entry` = 23895); diff --git a/src/server/scripts/World/transport_zeppelin.h b/src/server/scripts/World/transport_zeppelin.h index dd252a14e..6b9b6985d 100644 --- a/src/server/scripts/World/transport_zeppelin.h +++ b/src/server/scripts/World/transport_zeppelin.h @@ -36,6 +36,7 @@ enum ZeppelinEvent EVENT_UC_TO_OG_DEPARTURE = 15321, EVENT_UC_TO_GROMGOL_DEPARTURE = 15313, EVENT_GROMGOL_TO_UC_DEPARTURE = 15315, + EVENT_WK_DEPARTURE = 15430, }; enum ZeppelinMaster @@ -55,6 +56,11 @@ enum ZeppelinMaster NPC_KRENDLE_BIGPOCKETS = 34766, }; +enum ZeppelinTransport +{ + GO_WESTGUARD_ZEPPELIN = 186371, +}; + const float SEARCH_RANGE_ZEPPELIN_MASTER = 32.0f; enum ZeppelinPassenger diff --git a/src/server/scripts/World/transport_zeppelins.cpp b/src/server/scripts/World/transport_zeppelins.cpp index 3edbceca9..ca9121ccd 100644 --- a/src/server/scripts/World/transport_zeppelins.cpp +++ b/src/server/scripts/World/transport_zeppelins.cpp @@ -15,8 +15,15 @@ * with this program. If not, see . */ +#include "CreatureScript.h" #include "GameObjectAI.h" #include "GameObjectScript.h" +#include "Map.h" +#include "Player.h" +#include "ScriptedGossip.h" +#include "TaskScheduler.h" +#include "Transport.h" +#include "TransportMgr.h" #include "WorldState.h" #include "transport_zeppelin.h" @@ -101,9 +108,166 @@ struct go_transport_the_purple_princess : GameObjectAI } }; +// 186371 Zeppelin - Westguard Keep to Shattered Straits +struct go_transport_westguard_zeppelin : GameObjectAI +{ + go_transport_westguard_zeppelin(GameObject* object) : GameObjectAI(object) { }; + + void EventInform(uint32 eventId) override + { + if (eventId != EVENT_WK_ARRIVAL) + return; + + if (Creature* creature = me->FindNearestCreature(NPC_HARROWMEISER, SEARCH_RANGE_ZEPPELIN_MASTER)) + creature->AI()->Talk(0); + + // the dock is covered by two stop frames of 60s each, so EVENT_WK_DEPARTURE + // only fires two minutes later - warn one minute before it does + _scheduler.Schedule(1min, [this](TaskContext /*context*/) + { + if (Creature* creature = me->FindNearestCreature(NPC_HARROWMEISER, SEARCH_RANGE_ZEPPELIN_MASTER)) + creature->AI()->Talk(1); + }); + } + + void UpdateAI(uint32 diff) override + { + _scheduler.Update(diff); + } + +private: + TaskScheduler _scheduler; +}; + +enum WestguardZeppelinGossip +{ + GOSSIP_TEXT_PETROV_EN_ROUTE = 11322, + GOSSIP_TEXT_PETROV_ARRIVING = 11324, + GOSSIP_TEXT_HARROWMEISER_DOCKED = 11328, + GOSSIP_TEXT_HARROWMEISER_ARRIVING = 11330, + GOSSIP_TEXT_HARROWMEISER_EN_ROUTE = 11332, +}; + +// The two "en route" texts above read "in less than $3078w minutes". The client +// substitutes that token with the world state below, so it renders 0 unless a value +// has been pushed to the player first. +enum WestguardZeppelinWorldState +{ + WORLD_STATE_ZEPPELIN_ETA = 3078, +}; + +enum WestguardZeppelinState +{ + ZEPPELIN_STATE_EN_ROUTE, + ZEPPELIN_STATE_ARRIVING, + ZEPPELIN_STATE_DOCKED, +}; + +// Reports where the Westguard Keep zeppelin is in its path. While it is still touring the +// bay the remaining minutes are pushed to the player as WORLD_STATE_ZEPPELIN_ETA. +WestguardZeppelinState GetWestguardZeppelinState(Player* player, Map* map) +{ + MotionTransport const* zeppelin = nullptr; + for (Transport* transport : map->GetAllTransports()) + if (transport->GetEntry() == GO_WESTGUARD_ZEPPELIN) + { + zeppelin = transport->ToMotionTransport(); + break; + } + + if (!zeppelin || !zeppelin->GetPeriod()) + return ZEPPELIN_STATE_EN_ROUTE; + + uint32 dockArriveTime = 0; + uint32 dockDepartTime = 0; + for (KeyFrame const& frame : zeppelin->GetKeyFrames()) + { + if (frame.Node->arrivalEventID == EVENT_WK_ARRIVAL) + dockArriveTime = frame.ArriveTime; + else if (frame.Node->departureEventID == EVENT_WK_DEPARTURE) + dockDepartTime = frame.DepartureTime; + } + + if (!dockArriveTime) + return ZEPPELIN_STATE_EN_ROUTE; + + uint32 const period = zeppelin->GetPeriod(); + uint32 const timer = zeppelin->GetPathProgress() % period; + + // the dock is covered by the last and the first stop frame of the path, so the docked + // window normally wraps the end of one period into the start of the next + bool const docked = dockArriveTime > dockDepartTime + ? timer >= dockArriveTime || timer < dockDepartTime + : timer >= dockArriveTime && timer < dockDepartTime; + + if (docked) + return ZEPPELIN_STATE_DOCKED; + + // modular so it stays correct whichever way around the two stop frames sit + uint32 const msUntilArrival = (dockArriveTime + period - timer) % period; + if (msUntilArrival <= MINUTE * IN_MILLISECONDS) + return ZEPPELIN_STATE_ARRIVING; + + // the texts read "less than X minutes", so round up + player->SendUpdateWorldState(WORLD_STATE_ZEPPELIN_ETA, + (msUntilArrival + MINUTE * IN_MILLISECONDS - 1) / (MINUTE * IN_MILLISECONDS)); + + return ZEPPELIN_STATE_EN_ROUTE; +} + +// 23823 Harrowmeiser +class npc_harrowmeiser : public CreatureScript +{ +public: + npc_harrowmeiser() : CreatureScript("npc_harrowmeiser") { } + + bool OnGossipHello(Player* player, Creature* creature) override + { + uint32 textId = GOSSIP_TEXT_HARROWMEISER_EN_ROUTE; + switch (GetWestguardZeppelinState(player, creature->GetMap())) + { + case ZEPPELIN_STATE_DOCKED: + textId = GOSSIP_TEXT_HARROWMEISER_DOCKED; + break; + case ZEPPELIN_STATE_ARRIVING: + textId = GOSSIP_TEXT_HARROWMEISER_ARRIVING; + break; + default: + break; + } + + SendGossipMenuFor(player, textId, creature->GetGUID()); + return true; + } +}; + +// 23895 Bombardier Petrov +class npc_bombardier_petrov : public CreatureScript +{ +public: + npc_bombardier_petrov() : CreatureScript("npc_bombardier_petrov") { } + + bool OnGossipHello(Player* player, Creature* creature) override + { + if (creature->IsQuestGiver()) + player->PrepareQuestMenu(creature->GetGUID()); + + // he has no docked text of his own, "she's almost here" covers being at the dock too + uint32 const textId = GetWestguardZeppelinState(player, creature->GetMap()) == ZEPPELIN_STATE_EN_ROUTE + ? GOSSIP_TEXT_PETROV_EN_ROUTE + : GOSSIP_TEXT_PETROV_ARRIVING; + + SendGossipMenuFor(player, textId, creature->GetGUID()); + return true; + } +}; + void AddSC_transport_zeppelins() { RegisterGameObjectAI(go_transport_the_iron_eagle); RegisterGameObjectAI(go_transport_the_thundercaller); RegisterGameObjectAI(go_transport_the_purple_princess); + RegisterGameObjectAI(go_transport_westguard_zeppelin); + new npc_harrowmeiser(); + new npc_bombardier_petrov(); } From 4e6986fb4eb79f7a8b9866061900604a8785f2fd Mon Sep 17 00:00:00 2001 From: sogladev Date: Fri, 31 Jul 2026 11:05:58 +0200 Subject: [PATCH 67/69] fix(Scripts/ObsidianSanctum): Lava Strike (#26864) --- .../rev_1720754941115203700.sql | 2 ++ .../game/Spells/SpellInfoCorrections.cpp | 12 ------- .../ObsidianSanctum/boss_sartharion.cpp | 32 +++++++++++++------ 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1720754941115203700.sql diff --git a/data/sql/updates/pending_db_world/rev_1720754941115203700.sql b/data/sql/updates/pending_db_world/rev_1720754941115203700.sql new file mode 100644 index 000000000..d49205790 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1720754941115203700.sql @@ -0,0 +1,2 @@ +DELETE FROM `spell_script_names` WHERE `spell_id` = 57572 AND `ScriptName` = 'spell_sartharion_lava_strike'; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (57572, 'spell_sartharion_lava_strike'); diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index 9f2347215..b6b7686a6 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -1778,18 +1778,6 @@ void SpellMgr::LoadSpellInfoCorrections() spellInfo->Effects[EFFECT_0].TargetB = SpellImplicitTargetInfo(TARGET_UNIT_SRC_AREA_ALLY); }); - // Lava Strike damage - ApplySpellFix({ 57697 }, [](SpellInfo* spellInfo) - { - spellInfo->Effects[EFFECT_0].TargetA = SpellImplicitTargetInfo(TARGET_DEST_DEST); - }); - - // Lava Strike trigger - ApplySpellFix({ 57578 }, [](SpellInfo* spellInfo) - { - spellInfo->MaxAffectedTargets = 1; - }); - // Gift of Twilight Shadow/Fire ApplySpellFix({ 57835, 58766 }, [](SpellInfo* spellInfo) { diff --git a/src/server/scripts/Northrend/ChamberOfAspects/ObsidianSanctum/boss_sartharion.cpp b/src/server/scripts/Northrend/ChamberOfAspects/ObsidianSanctum/boss_sartharion.cpp index baa8c1596..21fe93ad7 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/ObsidianSanctum/boss_sartharion.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/ObsidianSanctum/boss_sartharion.cpp @@ -84,6 +84,7 @@ enum Spells SPELL_SARTHARION_FLAME_BREATH = 56908, SPELL_SARTHARION_TAIL_LASH = 56910, SPELL_CYCLONE_AURA_PERIODIC = 57598, + SPELL_LAVA_STRIKE = 57571, SPELL_LAVA_STRIKE_DUMMY = 57578, SPELL_LAVA_STRIKE_DUMMY_TRIGGER = 57697, SPELL_LAVA_STRIKE_SUMMON = 57572, @@ -213,11 +214,11 @@ const Position TenebronEggsPos[2][MAX_TENEBORN_EGGS_SUMMONS] = const Position CycloneSummonPos[MAX_CYCLONE_COUNT] = { - { 3235.28f, 591.180f, 57.0833f, 0.59037f }, - { 3200.97f, 480.929f, 57.0833f, 5.86197f }, - { 3281.57f, 507.984f, 57.0833f, 5.54346f }, - { 3210.11f, 531.957f, 57.0833f, 3.76777f }, - { 3286.42f, 585.010f, 57.0833f, 4.10307f }, + { 3238.55f, 589.14f, 57.0f, 0.59037f }, + { 3209.70f, 475.85f, 57.0f, 5.86197f }, + { 3282.10f, 504.02f, 57.0f, 5.54346f }, + { 3209.42f, 532.55f, 57.0f, 3.76777f }, + { 3283.50f, 581.75f, 57.0f, 4.10307f }, }; const Position AreaTriggerSummonPos[MAX_AREA_TRIGGER_COUNT] = @@ -1380,20 +1381,23 @@ class spell_sartharion_lava_strike : public SpellScript bool Load() override { _spawned = false; + _dummyFired = false; return true; } void HandleDummy(SpellEffIndex /*effIndex*/) { - if (!GetCaster() || !GetHitUnit()) + if (!GetCaster() || !GetHitUnit() || _dummyFired) return; - GetCaster()->CastSpell(GetHitUnit()->GetPositionX(), GetHitUnit()->GetPositionY(), GetHitUnit()->GetPositionZ(), SPELL_LAVA_STRIKE_DUMMY_TRIGGER, true); + _dummyFired = true; + + GetCaster()->CastSpell(GetHitUnit(), SPELL_LAVA_STRIKE, true); } void HandleSchoolDamage(SpellEffIndex /*effIndex*/) { - if (!GetCaster() || !GetHitUnit() || _spawned) + if (!GetCaster() || !GetHitUnit() || !GetHitUnit()->IsPlayer() || _spawned) return; if (InstanceScript* instance = GetCaster()->GetInstanceScript()) @@ -1402,21 +1406,31 @@ class spell_sartharion_lava_strike : public SpellScript { sarth->AI()->SetData(DATA_VOLCANO_BLOWS, GetHitUnit()->GetGUID().GetCounter()); sarth->CastSpell(GetHitUnit(), SPELL_LAVA_STRIKE_SUMMON, true); - _spawned = true; } } + + _spawned = true; + } + + void HandleSummon(SpellEffIndex effIndex) + { + if (GetCaster()->GetEntry() != NPC_SARTHARION) + PreventHitEffect(effIndex); } void Register() override { if (m_scriptSpellId == SPELL_LAVA_STRIKE_DUMMY) OnEffectHitTarget += SpellEffectFn(spell_sartharion_lava_strike::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); + else if (m_scriptSpellId == SPELL_LAVA_STRIKE_SUMMON) + OnEffectHit += SpellEffectFn(spell_sartharion_lava_strike::HandleSummon, EFFECT_0, SPELL_EFFECT_SUMMON); else OnEffectHitTarget += SpellEffectFn(spell_sartharion_lava_strike::HandleSchoolDamage, EFFECT_0, SPELL_EFFECT_SCHOOL_DAMAGE); } private: bool _spawned{false}; + bool _dummyFired{false}; }; // 57491 - Flame Tsunami From 8cfba48829e55c29c29dce73f3d9c1a03ba485b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 31 Jul 2026 09:06:35 +0000 Subject: [PATCH 68/69] chore(DB): import pending files Referenced commit(s): 21abfe0bc42ea629d8bfbf32ccd4a05f06822073 --- .../rev_1720754941115203700.sql => db_world/2026_07_31_01.sql} | 1 + .../rev_1785371667750135300.sql => db_world/2026_07_31_02.sql} | 1 + 2 files changed, 2 insertions(+) rename data/sql/updates/{pending_db_world/rev_1720754941115203700.sql => db_world/2026_07_31_01.sql} (83%) rename data/sql/updates/{pending_db_world/rev_1785371667750135300.sql => db_world/2026_07_31_02.sql} (96%) diff --git a/data/sql/updates/pending_db_world/rev_1720754941115203700.sql b/data/sql/updates/db_world/2026_07_31_01.sql similarity index 83% rename from data/sql/updates/pending_db_world/rev_1720754941115203700.sql rename to data/sql/updates/db_world/2026_07_31_01.sql index d49205790..fc806e20f 100644 --- a/data/sql/updates/pending_db_world/rev_1720754941115203700.sql +++ b/data/sql/updates/db_world/2026_07_31_01.sql @@ -1,2 +1,3 @@ +-- DB update 2026_07_31_00 -> 2026_07_31_01 DELETE FROM `spell_script_names` WHERE `spell_id` = 57572 AND `ScriptName` = 'spell_sartharion_lava_strike'; INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (57572, 'spell_sartharion_lava_strike'); diff --git a/data/sql/updates/pending_db_world/rev_1785371667750135300.sql b/data/sql/updates/db_world/2026_07_31_02.sql similarity index 96% rename from data/sql/updates/pending_db_world/rev_1785371667750135300.sql rename to data/sql/updates/db_world/2026_07_31_02.sql index 2a7ddf0b6..8acd0fe69 100644 --- a/data/sql/updates/pending_db_world/rev_1785371667750135300.sql +++ b/data/sql/updates/db_world/2026_07_31_02.sql @@ -1,3 +1,4 @@ +-- DB update 2026_07_31_01 -> 2026_07_31_02 -- -- Zeppelin, Alliance - Westguard Keep to Shattered Straits (taxi path 727) UPDATE `gameobject_template` SET `ScriptName` = 'go_transport_westguard_zeppelin' WHERE (`entry` = 186371); From bf8f6ebe5a172e656475f003425cbe55b1a8514e Mon Sep 17 00:00:00 2001 From: sudlud Date: Sat, 1 Aug 2026 06:43:06 +0200 Subject: [PATCH 69/69] fix(Core/Authserver): list the possible causes of an empty realm list (#26887) Co-authored-by: Ludwig Kormann Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/server/apps/authserver/Main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index d19e2b83c..8b5504e87 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -143,7 +143,11 @@ int main(int argc, char** argv) if (sRealmList->GetRealms().empty()) { - LOG_ERROR("server.authserver", "No valid realms specified."); + LOG_ERROR("server.authserver", "No valid realms specified. Possible reasons:"); + LOG_ERROR("server.authserver", "- the realmlist table of the auth database is empty"); + LOG_ERROR("server.authserver", "- every realm has flag 3 (REALM_FLAG_VERSION_MISMATCH | REALM_FLAG_OFFLINE), which the realm list " + "query excludes. Reset it with: UPDATE realmlist SET flag = 0 WHERE id = ;"); + LOG_ERROR("server.authserver", "- no realm address could be resolved, see the resolver errors logged above"); return 1; }