diff --git a/data/sql/updates/db_characters/2025_03_09_00.sql b/data/sql/updates/db_characters/2025_03_09_00.sql new file mode 100644 index 00000000000000..0a4b55e06e9458 --- /dev/null +++ b/data/sql/updates/db_characters/2025_03_09_00.sql @@ -0,0 +1,66 @@ +-- DB update 2025_02_16_00 -> 2025_03_09_00 +-- +DROP TABLE IF EXISTS `mail_server_template_items`; +CREATE TABLE `mail_server_template_items` ( + `id` INT UNSIGNED AUTO_INCREMENT, + `templateID` INT UNSIGNED NOT NULL, + `faction` ENUM('Alliance', 'Horde') NOT NULL, + `item` INT UNSIGNED NOT NULL, + `itemCount` INT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_mail_template` + FOREIGN KEY (`templateID`) REFERENCES `mail_server_template`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB COLLATE='utf8mb4_unicode_ci'; + +DELETE FROM `mail_server_template_items` WHERE `faction` = 'Alliance'; +INSERT INTO `mail_server_template_items` (`templateID`, `faction`, `item`, `itemCount`) +SELECT `id`, 'Alliance', `itemA`, `itemCountA` FROM `mail_server_template` WHERE `itemA` > 0; + +DELETE FROM `mail_server_template_items` WHERE `faction` = 'Horde'; +INSERT INTO `mail_server_template_items` (`templateID`, `faction`, `item`, `itemCount`) +SELECT `id`, 'Horde', `itemH`, `itemCountH` FROM `mail_server_template` WHERE `itemH` > 0; + +ALTER TABLE `mail_server_template` + DROP COLUMN `itemA`, + DROP COLUMN `itemCountA`, + DROP COLUMN `itemH`, + DROP COLUMN `itemCountH`; + +-- mail_server_template_conditions +DROP TABLE IF EXISTS `mail_server_template_conditions`; +CREATE TABLE `mail_server_template_conditions` ( + `id` INT UNSIGNED AUTO_INCREMENT, + `templateID` INT UNSIGNED NOT NULL, + `conditionType` ENUM('Level', 'PlayTime', 'Quest', 'Achievement', 'Reputation', 'Faction', 'Race', 'Class') NOT NULL, + `conditionValue` INT UNSIGNED NOT NULL, + `conditionState` INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + CONSTRAINT `fk_mail_template_conditions` + FOREIGN KEY (`templateID`) REFERENCES `mail_server_template`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB COLLATE='utf8mb4_unicode_ci'; + +DELETE FROM `mail_server_template_conditions` WHERE `conditionType` = 'Level'; +INSERT INTO `mail_server_template_conditions` (`templateID`, `conditionType`, `conditionValue`) +SELECT `id`, 'Level', `reqLevel` FROM `mail_server_template` WHERE `reqLevel` > 0; + +DELETE FROM `mail_server_template_conditions` WHERE `conditionType` = 'PlayTime'; +INSERT INTO `mail_server_template_conditions` (`templateID`, `conditionType`, `conditionValue`) +SELECT `id`, 'PlayTime', `reqPlayTime` FROM `mail_server_template` WHERE `reqPlayTime` > 0; + +ALTER TABLE `mail_server_template` + DROP COLUMN `reqLevel`, + DROP COLUMN `reqPlayTime`; + +-- mail_server_character +-- Make sure we dont have invalid instances in mail_server_character.mailId before we add the foregin key to avoid SQL errors +DELETE FROM `mail_server_character` WHERE `mailId` NOT IN (SELECT `id` FROM `mail_server_template`); + +-- Add foreign key for mail_server_character.mailId +ALTER TABLE `mail_server_character` + DROP PRIMARY KEY, + ADD PRIMARY KEY (`guid`, `mailId`), + ADD CONSTRAINT `fk_mail_server_character` + FOREIGN KEY (`mailId`) REFERENCES `mail_server_template`(`id`) + ON DELETE CASCADE; diff --git a/data/sql/updates/db_world/2025_03_08_01.sql b/data/sql/updates/db_world/2025_03_08_01.sql new file mode 100644 index 00000000000000..935b90996ba0ca --- /dev/null +++ b/data/sql/updates/db_world/2025_03_08_01.sql @@ -0,0 +1,4 @@ +-- DB update 2025_03_08_00 -> 2025_03_08_01 +-- +UPDATE `creature_template` SET `AIName` = '', `ScriptName` = 'npc_sunblade_scout' WHERE (`entry` = 25372); +DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 25372); diff --git a/data/sql/updates/db_world/2025_03_09_00.sql b/data/sql/updates/db_world/2025_03_09_00.sql new file mode 100644 index 00000000000000..e49c77b9f2bfa2 --- /dev/null +++ b/data/sql/updates/db_world/2025_03_09_00.sql @@ -0,0 +1,70 @@ +-- DB update 2025_03_08_01 -> 2025_03_09_00 +-- add creature +SET @CGUID := 502; +DELETE FROM `creature` WHERE `id1` = 26280 AND `guid` = @CGUID; +INSERT INTO `creature` (`guid`, `id1`, `id2`, `id3`, `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`, `VerifiedBuild`, `CreateObject`, `Comment`) VALUES +(@CGUID, 26280, 0, 0, 571, 65, 4157, 1, 1, 1, 3373.642822265625, 2584.31494140625, 42.15882492065429687, 1.623156189918518066, 120, 0, 0, 9291, 3231, 0, 0, 0, 0, '', 58629, 0, 'has guid specific SAI'); + +-- This creature is not a fighting stance +DELETE FROM `creature_addon` WHERE (`guid` IN (@CGUID)); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(@CGUID, 0, 0, 0, 1, 0, 0, ''); + +-- Dragonblight Mage Hunter with guid 502 smart ai +SET @ENTRY := -1 * @CGUID; +UPDATE `creature_template` SET `AIName` = 'SmartAI', `ScriptName` = '' WHERE `entry` = 26280; +DELETE FROM `smart_scripts` WHERE `source_type` = 0 AND `entryOrGuid` = @ENTRY; +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_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(@ENTRY, 0, 0, 0, 21, 0, 100, 0, 0, 0, 0, 0, 0, 0, 45, 0, 1, 0, 0, 0, 0, 10, 200124, 26496, 0, 0, 0, 0, 0, 'On home reached - Creature (26496) with guid 200124 (fetching): Set creature data #0 to 1'), +(@ENTRY, 0, 1, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 45, 0, 1, 0, 0, 0, 0, 10, 200124, 26496, 0, 0, 0, 0, 0, 'On respawn - Creature(26496) with guid 200124 (fetching): Set creature data #0 to 1'), +(@ENTRY, 0, 2, 0, 38, 0, 100, 0, 0, 1, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'Dragonblight Mage Hunter - On Data Set 0 1 - Respawn Self'); + +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 22 AND `SourceEntry` = @ENTRY AND `SourceId` = 0; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `Comment`) VALUES +(22, 3, @ENTRY, 0, 0, 36, 1, 0, 0, 0, 1, 'Dragonblight Mage Hunter must be dead to execute SAI (respawn)'); + +-- 134217728 - CREATURE_FLAG_EXTRA_DONT_OVERRIDE_SAI_ENTRY +UPDATE `creature_template` SET `flags_extra`=`flags_extra`|134217728 WHERE (`entry` = 26280); + +-- Wind Trader Mu'fah smart ai +SET @ENTRY := 26496; +UPDATE `creature_template` SET `AIName` = 'SmartAI', `ScriptName` = '' WHERE `entry` = @ENTRY; +DELETE FROM `smart_scripts` WHERE `source_type` = 0 AND `entryOrGuid` = @ENTRY; +DELETE FROM `smart_scripts` WHERE `source_type` = 9 AND `entryOrGuid` IN (@ENTRY * 100); +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_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(@ENTRY, 0, 0, 0, 9, 0, 100, 0, 0, 0, 11000, 15000, 0, 20, 11, 51817, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 'Wind Trader Mu\'fah - Within 0-20 Range - Cast \'Typhoon\''), +(@ENTRY, 0, 1, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 'On aggro - Self: Talk 1 to Attacked unit'), +(@ENTRY, 0, 2, 3, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 45, 0, 1, 0, 0, 0, 0, 10, @CGUID, 26280, 0, 0, 0, 0, 0, 'On respawn - Creature (26280) with guid 502 (fetching): Set creature data #0 to 1'), +(@ENTRY, 0, 3, 0, 61, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 12980, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'Wind Trader Mu\'fah - On Respawn - Cast \'Simple Teleport\''), +(@ENTRY, 0, 4, 0, 1, 0, 100, 0, 1000, 1000, 45000, 60000, 0, 0, 80, @ENTRY * 100, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'Wind Trader Mu\'fah - Out of Combat - Run Script'), +(@ENTRY * 100, 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, 'Wind Trader Mu\'fah Talk 0 to invoker'), +(@ENTRY * 100, 9, 1, 0, 0, 0, 100, 0, 8000, 8000, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 10, @CGUID, 26280, 0, 0, 0, 0, 0, 'Creature with guid 502 (fetching): Talk 0 to invoker'), +(@ENTRY, 0, 5, 0, 21, 0, 100, 0, 0, 0, 0, 0, 0, 0, 45, 0, 1, 0, 0, 0, 0, 10, @CGUID, 26280, 0, 0, 0, 0, 0, 'On home reached - Creature (26280) with guid 502 (fetching): Set creature data #0 to 1'), +(@ENTRY, 0, 6, 0, 38, 0, 100, 0, 0, 1, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'On data[0] set to 1 - Self: Set respawn timer to 0 ms'); + +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 22 AND `SourceEntry` = @ENTRY AND `SourceId` = 0; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `Comment`) VALUES +(22, 7, @ENTRY, 0, 0, 36, 1, 0, 0, 0, 1, 'Wind Trader Mu\'fah must be dead to execute SAI (respawn)'); + +DELETE FROM `creature_text` WHERE `CreatureID` = 26496; +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(26496, 0, 0, 'You will tell your commander that I will not wait a moment longer! Does he want this alliance or not?!', 12, 0, 100, 396, 0, 0, 25707, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 1, 'I have been waiting here for an eternity since our last meeting! When will Goramosh be done with his ritual?', 12, 0, 100, 396, 0, 0, 25708, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 2, 'This is insufferable! I represent a nexus-prince, I am not to be made to wait! Goramosh should be waiting on me!', 12, 0, 100, 15, 0, 0, 25710, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 3, 'I was personally assured by Malygos that your commander would make the details of this accord his top priority. Now let me pass!', 12, 0, 100, 15, 0, 0, 25711, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 4, 'If I have to wait much longer, I will be forced to reconsider the proposed accord between the Ethereum and the Blue Dragonflight!', 12, 0, 100, 15, 0, 0, 25712, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 5, 'You are nothing but an underling! I will not wait a moment longer! Escort me to Goramosh immediately!', 12, 0, 100, 396, 0, 0, 25713, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 6, 'Yes, yes, I am given to understand that the ley line has already been diverted. I will wait no longer... summon Goramosh this instant!', 12, 0, 100, 15, 0, 0, 25735, 0, 'Wind Trader Mu\'fah'), +(26496, 0, 7, 'Your surge needle appears to have worked perfectly. Surely Goramosh now has time to speak further of the proposed accord?', 12, 0, 100, 396, 0, 0, 25736, 0, 'Wind Trader Mu\'fah'), +(26496, 1, 0, 'What\'s this, more delays?!', 12, 0, 100, 0, 0, 0, 25715, 0, 'Wind Trader Mu\'fah'); + +DELETE FROM `creature_text` WHERE `CreatureID` = 26280; +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(26280, 0, 0, 'I cannot.', 12, 0, 100, 396, 0, 0, 25912, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 1, 'No.', 12, 0, 100, 396, 0, 0, 25913, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 2, 'You\'ll have to be patient.', 12, 0, 100, 396, 0, 0, 25914, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 3, 'Goramosh is busy with a ritual right now.', 12, 0, 100, 396, 0, 0, 25915, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 4, 'My orders are that you must wait here.', 12, 0, 100, 396, 0, 0, 25916, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 5, 'The master will be with you momentarily.', 12, 0, 100, 396, 0, 0, 25917, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 6, 'It won\'t be long now.', 12, 0, 100, 396, 0, 0, 25918, 0, 'Dragonblight Mage Hunter'), +(26280, 0, 7, 'My apologies, ambassador. My orders were quite clear.', 12, 0, 100, 396, 0, 0, 25919, 0, 'Dragonblight Mage Hunter'); diff --git a/data/sql/updates/db_world/2025_03_09_01.sql b/data/sql/updates/db_world/2025_03_09_01.sql new file mode 100644 index 00000000000000..c956b1fe69450f --- /dev/null +++ b/data/sql/updates/db_world/2025_03_09_01.sql @@ -0,0 +1,3 @@ +-- DB update 2025_03_09_00 -> 2025_03_09_01 +-- Increase Fists of fury proc chance to approx. 5% +UPDATE `spell_proc_event` SET `ppmRate` = 1.5 WHERE (`entry` = 41989); diff --git a/deps/zlib/inflate.c b/deps/zlib/inflate.c index 7be8c63662a7f4..7a728974923a43 100644 --- a/deps/zlib/inflate.c +++ b/deps/zlib/inflate.c @@ -763,9 +763,10 @@ int flush; copy = state->length; if (copy > have) copy = have; if (copy) { + len = state->head->extra_len - state->length; if (state->head != Z_NULL && - state->head->extra != Z_NULL) { - len = state->head->extra_len - state->length; + state->head->extra != Z_NULL && + len < state->head->extra_max) { zmemcpy(state->head->extra + len, next, len + copy > state->head->extra_max ? state->head->extra_max - len : copy); diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 734496fcf8aa85..2e55c55b08e6c8 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -3928,7 +3928,7 @@ void Unit::_UpdateAutoRepeatSpell() static uint32 const HUNTER_AUTOSHOOT = 75; // Check "realtime" interrupts - if ((IsPlayer() && ToPlayer()->isMoving()) || IsNonMeleeSpellCast(false, false, true, spellProto->Id == HUNTER_AUTOSHOOT)) + if ((IsPlayer() && ToPlayer()->isMoving() && spellProto->Id != HUNTER_AUTOSHOOT) || IsNonMeleeSpellCast(false, false, true, spellProto->Id == HUNTER_AUTOSHOOT)) { // cancel wand shoot if (spellProto->Id != HUNTER_AUTOSHOOT) @@ -3937,7 +3937,8 @@ void Unit::_UpdateAutoRepeatSpell() return; } - if (m_AutoRepeatFirstCast && getAttackTimer(RANGED_ATTACK) < 500) + // Apply delay (Hunter's autoshoot not affected) + if (m_AutoRepeatFirstCast && getAttackTimer(RANGED_ATTACK) < 500 && spellProto->Id != HUNTER_AUTOSHOOT) { setAttackTimer(RANGED_ATTACK, 500); } diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 47cdd1cf7dd8ae..a4033ca1cb88dd 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -10297,116 +10297,3 @@ uint32 ObjectMgr::GetQuestMoneyReward(uint8 level, uint32 questMoneyDifficulty) return 0; } - -void ObjectMgr::SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, uint32 rewardItemA, uint32 rewardItemCountA, uint32 rewardItemH, uint32 rewardItemCountH, std::string subject, std::string body, uint8 active) const -{ - if (active) - { - if (player->GetLevel() < reqLevel) - return; - - if (player->GetTotalPlayedTime() < reqPlayTime) - return; - - CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); - - MailSender sender(MAIL_NORMAL, player->GetGUID().GetCounter(), MAIL_STATIONERY_GM); - MailDraft draft(subject, body); - - draft.AddMoney(player->GetTeamId() == TEAM_ALLIANCE ? rewardMoneyA : rewardMoneyH); - if (Item* mailItem = Item::CreateItem(player->GetTeamId() == TEAM_ALLIANCE ? rewardItemA : rewardItemH, player->GetTeamId() == TEAM_ALLIANCE ? rewardItemCountA : rewardItemCountH)) - { - mailItem->SaveToDB(trans); - draft.AddItem(mailItem); - } - - draft.SendMailTo(trans, MailReceiver(player), sender); - CharacterDatabase.CommitTransaction(trans); - - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_MAIL_SERVER_CHARACTER); - stmt->SetData(0, player->GetGUID().GetCounter()); - stmt->SetData(1, id); - CharacterDatabase.Execute(stmt); - - LOG_DEBUG("entities.player", "ObjectMgr::SendServerMail() Sent mail id {} to {}", id, player->GetGUID().ToString()); - } -} - -void ObjectMgr::LoadMailServerTemplates() -{ - uint32 oldMSTime = getMSTime(); - - _serverMailStore.clear(); // for reload case - - // 0 1 2 3 4 5 6 7 8 9 10 11 - QueryResult result = CharacterDatabase.Query("SELECT `id`, `reqLevel`, `reqPlayTime`, `moneyA`, `moneyH`, `itemA`, `itemCountA`, `itemH`,`itemCountH`, `subject`, `body`, `active` FROM `mail_server_template`"); - if (!result) - { - LOG_INFO("sql.sql", ">> Loaded 0 server mail rewards. DB table `mail_server_template` is empty."); - LOG_INFO("server.loading", " "); - return; - } - - _serverMailStore.rehash(result->GetRowCount()); - - do - { - Field* fields = result->Fetch(); - - uint32 id = fields[0].Get(); - - ServerMail& servMail = _serverMailStore[id]; - - servMail.id = id; - servMail.reqLevel = fields[1].Get(); - servMail.reqPlayTime = fields[2].Get(); - servMail.moneyA = fields[3].Get(); - servMail.moneyH = fields[4].Get(); - servMail.itemA = fields[5].Get(); - servMail.itemCountA = fields[6].Get(); - servMail.itemH = fields[7].Get(); - servMail.itemCountH = fields[8].Get(); - servMail.subject = fields[9].Get(); - servMail.body = fields[10].Get(); - servMail.active = fields[11].Get(); - - if (servMail.reqLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has reqLevel {} but max level is {} for id {}, skipped.", servMail.reqLevel, sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL), servMail.id); - return; - } - - if (servMail.moneyA > MAX_MONEY_AMOUNT || servMail.moneyH > MAX_MONEY_AMOUNT) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has moneyA {} or moneyH {} larger than MAX_MONEY_AMOUNT {} for id {}, skipped.", servMail.moneyA, servMail.moneyH, MAX_MONEY_AMOUNT, servMail.id); - return; - } - - ItemTemplate const* itemTemplateA = sObjectMgr->GetItemTemplate(servMail.itemA); - if (!itemTemplateA && servMail.itemA) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has invalid item in itemA {} for id {}, skipped.", servMail.itemA, servMail.id); - return; - } - ItemTemplate const* itemTemplateH = sObjectMgr->GetItemTemplate(servMail.itemH); - if (!itemTemplateH && servMail.itemH) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has invalid item in itemH {} for id {}, skipped.", servMail.itemH, servMail.id); - return; - } - - if (!servMail.itemA && servMail.itemCountA) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has itemCountA {} with no ItemA, set to 0", servMail.itemCountA); - servMail.itemCountA = 0; - } - if (!servMail.itemH && servMail.itemCountH) - { - LOG_ERROR("sql.sql", "Table `mail_server_template` has itemCountH {} with no ItemH, set to 0", servMail.itemCountH); - servMail.itemCountH = 0; - } - } while (result->NextRow()); - - LOG_INFO("server.loading", ">> Loaded {} Mail Server Template in {} ms", _serverMailStore.size(), GetMSTimeDiffToNow(oldMSTime)); - LOG_INFO("server.loading", " "); -} diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 553bf3b2685cd9..8826ce801577df 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -665,7 +665,6 @@ typedef std::map, QuestGreeting> QuestGreetingContainer typedef std::unordered_map CacheVendorItemContainer; typedef std::unordered_map CacheTrainerSpellContainer; -typedef std::unordered_map ServerMailContainer; typedef std::vector CreatureCustomIDsContainer; @@ -1055,7 +1054,6 @@ class ObjectMgr void LoadInstanceTemplate(); void LoadInstanceEncounters(); void LoadMailLevelRewards(); - void LoadMailServerTemplates(); void LoadVehicleTemplateAccessories(); void LoadVehicleAccessories(); void LoadVehicleSeatAddon(); @@ -1188,8 +1186,6 @@ class ObjectMgr return nullptr; } - [[nodiscard]] ServerMailContainer const& GetAllServerMailStore() const { return _serverMailStore; } - [[nodiscard]] BroadcastText const* GetBroadcastText(uint32 id) const { BroadcastTextContainer::const_iterator itr = _broadcastTextStore.find(id); @@ -1449,7 +1445,6 @@ class ObjectMgr } [[nodiscard]] uint32 GetQuestMoneyReward(uint8 level, uint32 questMoneyDifficulty) const; - void SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, uint32 rewardItemA, uint32 rewardItemCountA, uint32 rewardItemH, uint32 rewardItemCountH, std::string subject, std::string body, uint8 active) const; private: // first free id for selected id type uint32 _auctionId; // pussywizard: accessed by a single thread @@ -1614,8 +1609,6 @@ class ObjectMgr CacheVendorItemContainer _cacheVendorItemStore; CacheTrainerSpellContainer _cacheTrainerSpellStore; - ServerMailContainer _serverMailStore; - std::set _difficultyEntries[MAX_DIFFICULTY - 1]; // already loaded difficulty 1 value in creatures, used in CheckCreatureTemplate std::set _hasDifficultyEntries[MAX_DIFFICULTY - 1]; // already loaded creatures with difficulty 1 values, used in CheckCreatureTemplate diff --git a/src/server/game/Mails/Mail.h b/src/server/game/Mails/Mail.h index 17c078e259a8b6..f89a5e1271b8e2 100644 --- a/src/server/game/Mails/Mail.h +++ b/src/server/game/Mails/Mail.h @@ -210,21 +210,4 @@ struct Mail [[nodiscard]] bool IsReturnedMail() const { return checked & MAIL_CHECK_MASK_RETURNED; } }; -struct ServerMail -{ - ServerMail() = default; - uint32 id{ 0 }; - uint8 reqLevel{ 0 }; - uint32 reqPlayTime{ 0 }; - uint32 moneyA{ 0 }; - uint32 moneyH{ 0 }; - uint32 itemA{ 0 }; - uint32 itemCountA{ 0 }; - uint32 itemH{ 0 }; - uint32 itemCountH{ 0 }; - std::string subject; - std::string body; - uint8 active{ 0 }; -}; - #endif diff --git a/src/server/game/Mails/ServerMailMgr.cpp b/src/server/game/Mails/ServerMailMgr.cpp new file mode 100644 index 00000000000000..d11595066a16d8 --- /dev/null +++ b/src/server/game/Mails/ServerMailMgr.cpp @@ -0,0 +1,351 @@ +/* + * 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 "ServerMailMgr.h" +#include "AchievementMgr.h" +#include "DatabaseEnv.h" +#include "Item.h" +#include "Log.h" +#include "ObjectMgr.h" +#include "Player.h" +#include "QuestDef.h" +#include "SharedDefines.h" +#include "Timer.h" + +ServerMailMgr* ServerMailMgr::instance() +{ + static ServerMailMgr instance; + return &instance; +} + +void ServerMailMgr::LoadMailServerTemplates() +{ + uint32 oldMSTime = getMSTime(); + + _serverMailStore.clear(); // for reload case + + // 0 1 2 3 4 5 + QueryResult result = CharacterDatabase.Query("SELECT `id`, `moneyA`, `moneyH`, `subject`, `body`, `active` FROM `mail_server_template`"); + if (!result) + { + LOG_INFO("server.loading", ">> Loaded 0 server mail rewards. DB table `mail_server_template` is empty."); + LOG_INFO("server.loading", " "); + return; + } + + _serverMailStore.reserve(result->GetRowCount()); + + do + { + Field* fields = result->Fetch(); + + uint32 id = fields[0].Get(); + + ServerMail& servMail = _serverMailStore[id]; + servMail.id = id; + servMail.moneyA = fields[1].Get(); + servMail.moneyH = fields[2].Get(); + servMail.subject = fields[3].Get(); + servMail.body = fields[4].Get(); + servMail.active = fields[5].Get(); + + // Skip non-activated entries + if (!servMail.active) + continue; + + if (servMail.moneyA > MAX_MONEY_AMOUNT || servMail.moneyH > MAX_MONEY_AMOUNT) + { + LOG_ERROR("sql.sql", "Table `mail_server_template` has moneyA {} or moneyH {} larger than MAX_MONEY_AMOUNT {} for id {}, skipped.", servMail.moneyA, servMail.moneyH, MAX_MONEY_AMOUNT, servMail.id); + continue; + } + } while (result->NextRow()); + + LoadMailServerTemplatesItems(); + LoadMailServerTemplatesConditions(); + + LOG_INFO("server.loading", ">> Loaded {} Mail Server definitions in {} ms", _serverMailStore.size(), GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void ServerMailMgr::LoadMailServerTemplatesItems() +{ + // 0 1 2 3 + QueryResult result = CharacterDatabase.Query("SELECT `templateID`, `faction`, `item`, `itemCount` FROM `mail_server_template_items`"); + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 server mail items. DB table `mail_server_template_items` is empty."); + return; + } + + do + { + Field* fields = result->Fetch(); + + uint32 templateID = fields[0].Get(); + std::string_view faction = fields[1].Get(); + uint32 item = fields[2].Get(); + uint32 itemCount = fields[3].Get(); + + if (_serverMailStore.find(templateID) == _serverMailStore.end()) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has an invalid templateID {}, skipped.", templateID); + continue; + } + + ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item); + if (!itemTemplate) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has an invalid item {} for templateID {}, skipped.", item, templateID); + continue; + } + + if (!itemCount) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount 0 for item {}, skipped.", item); + continue; + } + + uint32 stackable = itemTemplate->Stackable; + if (itemCount > stackable) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount {} exceeding item_template.Stackable {} for item {}, skipped.", itemCount, stackable, item); + continue; + } + + uint32 maxCount = itemTemplate->MaxCount; + if (maxCount && itemCount > maxCount) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount {} exceeding item_template.MaxCount {} for item {}, skipped", itemCount, maxCount, item); + continue; + } + + ServerMailItems mailItem; + mailItem.item = item; + mailItem.itemCount = itemCount; + + if (faction == "Alliance") + _serverMailStore[templateID].itemsA.push_back(mailItem); + else if (faction == "Horde") + _serverMailStore[templateID].itemsH.push_back(mailItem); + else [[unlikely]] + { + LOG_ERROR("sql.sql", "Table `mail_server_template_items` has invalid faction value '{}' for templateID {}, skipped.", faction, templateID); + continue; + } + + } while (result->NextRow()); +} + +void ServerMailMgr::LoadMailServerTemplatesConditions() +{ + // 0 1 2 3 + QueryResult result = CharacterDatabase.Query("SELECT `templateID`, `conditionType`, `conditionValue`, `conditionState` FROM `mail_server_template_conditions`"); + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 server mail conditions. DB table `mail_server_template_conditions` is empty."); + return; + } + + do + { + Field* fields = result->Fetch(); + + uint32 templateID = fields[0].Get(); + std::string_view conditionTypeStr = fields[1].Get(); + uint32 conditionValue = fields[2].Get(); + uint32 conditionState = fields[3].Get(); + + if (_serverMailStore.find(templateID) == _serverMailStore.end()) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has an invalid templateID {}, skipped.", templateID); + continue; + } + + // Get conditiontype from ServerMailConditionTypePairs + ServerMailConditionType conditionType; + conditionType = GetServerMailConditionType(conditionTypeStr); + if (conditionType == ServerMailConditionType::Invalid) [[unlikely]] + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has unknown conditionType '{}', skipped.", conditionTypeStr); + continue; + } + + if (conditionState && !ConditionTypeUsesConditionState(conditionType)) + LOG_WARN("sql.sql", "Table `mail_server_template_conditions` has conditionState value ({}) for conditionType ({}) which does not use conditionState.", conditionState, conditionTypeStr); + + switch (conditionType) + { + case ServerMailConditionType::Level: + if (conditionValue > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Level' with invalid conditionValue ({}), max level is ({}) for templateID {}, skipped.", conditionValue, sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL), templateID); + continue; + } + break; + case ServerMailConditionType::Quest: + { + Quest const* qInfo = sObjectMgr->GetQuestTemplate(conditionValue); + if (!qInfo) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Quest' with invalid quest in conditionValue ({}) for templateID {}, skipped.", conditionValue, templateID); + continue; + } + if (conditionState < QUEST_STATUS_NONE || conditionState >= MAX_QUEST_STATUS || + /*2 and 4 not defined and should not be used*/ conditionState == 2 || conditionState == 4) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Quest' with invalid conditionState ({}) for templateID {}, skipped.", conditionState, templateID); + continue; + } + break; + } + case ServerMailConditionType::Achievement: + { + AchievementEntry const* achievement = sAchievementStore.LookupEntry(conditionValue); + if (!achievement) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Achievement' with invalid achievement in conditionValue ({}) for templateID {}, skipped.", conditionValue, templateID); + continue; + } + break; + } + case ServerMailConditionType::Reputation: + { + FactionEntry const* faction = sFactionStore.LookupEntry(conditionValue); + if (!faction) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Reputation' with invalid faction in conditionValue ({}) for templateID {}, skipped.", conditionValue, templateID); + continue; + } + if (conditionState < REP_HATED || conditionState > REP_EXALTED) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Reputation' with invalid conditionState ({}) for templateID {}, skipped.", conditionState, templateID); + continue; + } + break; + } + case ServerMailConditionType::Faction: + if (conditionValue < TEAM_ALLIANCE || conditionValue > TEAM_HORDE) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Faction' with invalid conditionValue ({}) for templateID {}, skipped.", conditionState, templateID); + continue; + } + break; + case ServerMailConditionType::Race: + if (conditionValue & ~RACEMASK_ALL_PLAYABLE) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Race' with invalid conditionValue ({}) for templateID {}, skipped.", conditionState, templateID); + continue; + } + break; + case ServerMailConditionType::Class: + if (conditionValue & ~CLASSMASK_ALL_PLAYABLE) + { + LOG_ERROR("sql.sql", "Table `mail_server_template_conditions` has conditionType 'Class' with invalid conditionValue ({}) for templateID {}, skipped.", conditionState, templateID); + continue; + } + break; + default: + break; + } + + ServerMailCondition condition; + condition.type = conditionType; + condition.value = conditionValue; + condition.state = conditionState; + _serverMailStore[templateID].conditions.push_back(condition); + + } while (result->NextRow()); +} + +void ServerMailMgr::SendServerMail(Player* player, uint32 id, uint32 money, + std::vector const& items, + std::vector const& conditions, + std::string const& subject, std::string const& body) const +{ + for (ServerMailCondition const& condition : conditions) + if (!condition.CheckCondition(player)) + return; + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + MailSender sender(MAIL_NORMAL, player->GetGUID().GetCounter(), MAIL_STATIONERY_GM); + MailDraft draft(subject, body); + + draft.AddMoney(money); + // Loop through all items and attach them to the mail + for (auto const& mailItem : items) + if (Item* newItem = Item::CreateItem(mailItem.item, mailItem.itemCount)) + { + newItem->SaveToDB(trans); + draft.AddItem(newItem); + } + + draft.SendMailTo(trans, MailReceiver(player), sender); + CharacterDatabase.CommitTransaction(trans); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_MAIL_SERVER_CHARACTER); + stmt->SetData(0, player->GetGUID().GetCounter()); + stmt->SetData(1, id); + CharacterDatabase.Execute(stmt); +} + +ServerMailConditionType ServerMailMgr::GetServerMailConditionType(std::string_view conditionTypeStr) const +{ + for (auto const& pair : ServerMailConditionTypePairs) + if (pair.first == conditionTypeStr) + return pair.second; + + return ServerMailConditionType::Invalid; +} + +bool ServerMailMgr::ConditionTypeUsesConditionState(ServerMailConditionType type) const +{ + switch (type) + { + case ServerMailConditionType::Quest: + case ServerMailConditionType::Reputation: + return true; + default: + return false; + } +} + +bool ServerMailCondition::CheckCondition(Player* player) const +{ + switch (type) + { + case ServerMailConditionType::Level: + return player->GetLevel() >= value; + case ServerMailConditionType::PlayTime: + return player->GetTotalPlayedTime() >= value; + case ServerMailConditionType::Quest: + return player->GetQuestStatus(value) == state; + case ServerMailConditionType::Achievement: + return player->HasAchieved(value); + case ServerMailConditionType::Reputation: + return player->GetReputationRank(value) >= state; + case ServerMailConditionType::Faction: + return player->GetTeamId() == value; + case ServerMailConditionType::Race: + return (player->getRaceMask() & value) != 0; + case ServerMailConditionType::Class: + return (player->getClassMask() & value) != 0; + default: + [[unlikely]] LOG_ERROR("server.mail", "Unknown server mail condition type '{}'", static_cast(type)); + return false; + } +} diff --git a/src/server/game/Mails/ServerMailMgr.h b/src/server/game/Mails/ServerMailMgr.h new file mode 100644 index 00000000000000..9cf70a8050df20 --- /dev/null +++ b/src/server/game/Mails/ServerMailMgr.h @@ -0,0 +1,231 @@ +/* + * 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 . + */ + +/** + * @file ServerMailMgr.h + * @brief Manages the ServerMail operations, including template loading, condition checking, and mail delivery. + * + * This class handles the loading of server mail templates, associated items, and conditions from the database. + * It also provides functionality to check player eligibility for receiving server mails based on configured conditions. + * + * Key features: + * - Supports multi-item mails via `mail_server_template_items` + * - Supports flexible mail conditions (level, playtime, quest, achievement) via `mail_server_template_conditions` + * - Ensures all related data is loaded and validated on startup + */ + +#ifndef _SERVERMAILMGR_H +#define _SERVERMAILMGR_H + +#include "Define.h" +#include +#include +#include + +class Player; + +/** + * @enum ServerMailConditionType + * @brief Represents the type of conditions that can be applied to server mail. + */ +enum class ServerMailConditionType : uint8 +{ + Invalid = 0, ///< Internal use, not used in DB. + Level = 1, ///< Requires the player to be at least a specific level. + PlayTime = 2, ///< Requires the player to have played for a minimum amount of time (in milliseconds). + Quest = 3, ///< Requires the player to have completed a specific quest. + Achievement = 4, ///< Requires the player to have earned a specific achievement. + Reputation = 5, ///< Requires the player to have earned reputation with a specific faction. + Faction = 6, ///< Requires the player to be a part of a specific faction. Horde/Alliance. + Race = 7, ///< Requires the player to be a specific race. + Class = 8, ///< Requires the player to be a specific class. +}; + +/** +* @brief Assign string condition to corresponding @ref ServerMailConditionType enum value +*/ +constexpr std::pair ServerMailConditionTypePairs[] = +{ + { "Level", ServerMailConditionType::Level }, + { "PlayTime", ServerMailConditionType::PlayTime }, + { "Quest", ServerMailConditionType::Quest }, + { "Achievement", ServerMailConditionType::Achievement }, + { "Reputation", ServerMailConditionType::Reputation }, + { "Faction", ServerMailConditionType::Faction }, + { "Race", ServerMailConditionType::Race }, + { "Class", ServerMailConditionType::Class } +}; + +/** + * @struct ServerMailCondition + * @brief Represents a condition that must be met for a player to receive a server mail. + * + * Each condition has a type (see @ref ServerMailConditionType) and a value associated with the type. + * For example, for a level condition, the value represents the required player level. + * + * Some condition also have a state associated with the value. + * For example, for a reputation condition, the state represents the current reputation state, like Exalted. + * + * Conditions are attached to server mail templates and are evaluated when players log in. + */ +struct ServerMailCondition +{ + ServerMailCondition() = default; + + ServerMailConditionType type = ServerMailConditionType::Invalid; + uint32 value{ 0 }; + uint32 state{ 0 }; + + /** + * @brief Checks if a player meets this condition. + * + * Evaluates the condition type and compares the player's attributes to the required value. + * + * @param player The player to check. + * @return True if the player meets the condition, otherwise false. + */ + bool CheckCondition(Player* player) const; +}; + +/** + * @struct ServerMailItems + * @brief Represents an item reward associated with a server mail template. + * + * Server mail templates can have multiple item rewards, stored separately for each faction. + * This struct tracks the item ID and item count. + */ +struct ServerMailItems +{ + ServerMailItems() = default; + uint32 item{ 0 }; + uint32 itemCount{ 0 }; +}; + +/** + * @struct ServerMail + * @brief Represents a server mail template, including rewards, conditions, and metadata. + * + * This structure defines a mail template that can be sent to players upon login, + * provided they meet the associated conditions. + */ +struct ServerMail +{ + ServerMail() = default; + uint32 id{ 0 }; + uint32 moneyA{ 0 }; + uint32 moneyH{ 0 }; + std::string subject; + std::string body; + uint8 active{ 0 }; + + // Conditions from mail_server_template_conditions + std::vector conditions; + + // Items from mail_server_template_items + std::vector itemsA; + std::vector itemsH; +}; + +typedef std::unordered_map ServerMailContainer; + +class ServerMailMgr +{ +private: + ServerMailMgr() = default; + ~ServerMailMgr() = default; +public: + static ServerMailMgr* instance(); + + /** + * @brief Loads all server mail templates from the database into memory. + * + * Queries the `mail_server_template` table and loads all rows into memory. + * This method is intended to be called during server startup. + */ + void LoadMailServerTemplates(); + + /** + * @brief Loads all items associated with server mail templates. + * + * Queries the `mail_server_template_items` table and loads all items into memory, + * linking them to their corresponding templates by template ID. + * This method is intended to be called during server startup. + */ + void LoadMailServerTemplatesItems(); + + /** + * @brief Loads all conditions associated with server mail templates. + * + * Queries the `mail_server_template_conditions` table and loads all conditions into memory, + * linking them to their corresponding templates by template ID. + * This method is intended to be called during server startup. + */ + void LoadMailServerTemplatesConditions(); + + /** + * @brief Convert DB value of conditionType to ServerMailConditionType. + * + * Lookup the corresponding SeverMailConditionType enum for the provided + * string by DB. If the string is not found we return internal default value + * ServerMailConditionType::Invalid + * + * @param conditionTypeStr string value from DB of conditionType + * @return ServerMailConditionType The corresponding value (see @ref ServerMailConditionType) or default to ServerMailConditionType::Invalid + */ + ServerMailConditionType GetServerMailConditionType(std::string_view conditionTypeStr) const; + + /** + * @brief Check if ConditionType should use ConditionState + * + * @return True if the ConditionType is allowed to use ConditionState, otherwise False. + */ + bool ConditionTypeUsesConditionState(ServerMailConditionType type) const; + + /** + * @brief Sends a server mail to a player if the template is active and the player is eligible. + * + * This method handles the creation of the mail, adding money and items, and saving the mail to the database. + * It also records that the player received the mail to prevent duplicate delivery. + * + * @param player The recipient player. + * @param id The template ID. + * @param money Money reward. + * @param items List of items to include in the mail. + * @param conditions List of the conditions for the mail. + * @param subject Mail subject. + * @param body Mail body. + * @param active Whether the mail template is active. + */ + void SendServerMail(Player* player, uint32 id, uint32 money, std::vector const& items, std::vector const& conditions, std::string const& subject, std::string const& body) const; + + /** + * @brief Retrieves the entire server mail store. + * + * This function returns a constant reference to the internal + * `_serverMailStore` container, which holds all server mail data. + * + * @return A constant reference to the `ServerMailContainer` containing all stored server mail. + */ + [[nodiscard]] ServerMailContainer const& GetAllServerMailStore() const { return _serverMailStore; } + +private: + ServerMailContainer _serverMailStore; +}; + +#define sServerMailMgr ServerMailMgr::instance() + +#endif // _SERVERMAILMGR_H diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index 29f9930c594d95..1252b05b0476d2 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -3678,12 +3678,6 @@ void SpellMgr::LoadSpellInfoCorrections() spellInfo->AttributesEx5 |= SPELL_ATTR5_ALWAYS_AOE_LINE_OF_SIGHT; }); - //Crushing the Crown - ApplySpellFix({ 71024 }, [](SpellInfo* spellInfo) - { - spellInfo->Effects[EFFECT_0].TargetA = SpellImplicitTargetInfo(TARGET_DEST_DYNOBJ_NONE); - }); - // Battle for the Undercity ApplySpellFix({ 59892 // Cyclone fall diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 097d4f655592e1..f7d415b8810ed1 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -73,6 +73,7 @@ #include "PoolMgr.h" #include "Realm.h" #include "ScriptMgr.h" +#include "ServerMailMgr.h" #include "SkillDiscovery.h" #include "SkillExtraItems.h" #include "SmartAI.h" @@ -1667,8 +1668,8 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Player Level Dependent Mail Rewards..."); sObjectMgr->LoadMailLevelRewards(); - LOG_INFO("server.loading", "Load Mail Server Template..."); - sObjectMgr->LoadMailServerTemplates(); + LOG_INFO("server.loading", "Load Mail Server definitions..."); + sServerMailMgr->LoadMailServerTemplates(); // Loot tables LoadLootTables(); diff --git a/src/server/scripts/Commands/cs_reload.cpp b/src/server/scripts/Commands/cs_reload.cpp index 45434d502aad93..8793cebd2ae3db 100644 --- a/src/server/scripts/Commands/cs_reload.cpp +++ b/src/server/scripts/Commands/cs_reload.cpp @@ -38,6 +38,7 @@ EndScriptData */ #include "MotdMgr.h" #include "ObjectMgr.h" #include "ScriptMgr.h" +#include "ServerMailMgr.h" #include "SkillDiscovery.h" #include "SkillExtraItems.h" #include "SmartAI.h" @@ -1196,7 +1197,7 @@ class reload_commandscript : public CommandScript static bool HandleReloadMailServerTemplateCommand(ChatHandler* handler) { LOG_INFO("server.loading", "Reloading `server_mail_template` table"); - sObjectMgr->LoadMailServerTemplates(); + sServerMailMgr->LoadMailServerTemplates(); handler->SendGlobalGMSysMessage("DB table `server_mail_template` reloaded."); return true; } diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/instance_sunwell_plateau.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/instance_sunwell_plateau.cpp index 34743573df13fa..b0d51b81485142 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/instance_sunwell_plateau.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/instance_sunwell_plateau.cpp @@ -15,9 +15,11 @@ * with this program. If not, see . */ +#include "CreatureScript.h" #include "InstanceMapScript.h" #include "InstanceScript.h" #include "Player.h" +#include "ScriptedCreature.h" #include "SpellScript.h" #include "SpellScriptLoader.h" #include "sunwell_plateau.h" @@ -148,8 +150,106 @@ class spell_cataclysm_breath : public SpellScript } }; +enum SunbladeScout +{ + NPC_SUNBLADE_PROTECTOR = 25507, + SAY_AGGRO = 0, // Enemies spotted! Attack while I try to activate a Protector! + SPELL_ACTIVATE_SUNBLADE_PROTECTOR = 46475, + SPELL_COSMETIC_STUN_IMMUNE_PERMANENT = 59123, + SPELL_FELBLOOD_CHANNEL = 46319, + SPELL_SINISTER_STRIKE = 46558, +}; + +struct npc_sunblade_scout : public ScriptedAI +{ + npc_sunblade_scout(Creature* creature) : ScriptedAI(creature) { } + + void Reset() override + { + scheduler.CancelAll(); + ScheduleOOC(); + me->SetCombatMovement(false); + me->SetReactState(REACT_AGGRESSIVE); + _protectorGUID.Clear(); + } + + void JustEngagedWith(Unit* /*who*/) override + { + scheduler.CancelAll(); + me->CallForHelp(30.0f); + Talk(SAY_AGGRO); + std::list protectors; + GetCreatureListWithEntryInGrid(protectors, me, NPC_SUNBLADE_PROTECTOR, 100.0f); // range unknown + // Skip already activated protectors + protectors.remove_if([](Creature* trigger) {return !trigger->HasAura(SPELL_COSMETIC_STUN_IMMUNE_PERMANENT);}); + protectors.sort(Acore::ObjectDistanceOrderPred(me)); + if (protectors.empty()) + ScheduleCombat(); + Creature* closestProtector = protectors.front(); + me->GetMotionMaster()->MoveFollow(closestProtector, 0.0f, 0.0f); + _protectorGUID = closestProtector->GetGUID(); + me->ClearTarget(); + me->SetReactState(REACT_PASSIVE); + scheduler.Schedule(1s, [this](TaskContext context) + { + if (_protectorGUID) + if (Creature* protector = ObjectAccessor::GetCreature(*me, _protectorGUID)) + { + if (me->IsWithinRange(protector, 25.0f)) + { + me->SetFacingToObject(protector); + DoCastSelf(SPELL_ACTIVATE_SUNBLADE_PROTECTOR); + scheduler.Schedule(5s, [this](TaskContext /*context*/) + { + ScheduleCombat(); + }); + return; + } + context.Repeat(1s); + return; + } + ScheduleCombat(); + }); + } + + void ScheduleCombat() + { + me->SetReactState(REACT_AGGRESSIVE); + me->SetCombatMovement(true); + if (Unit* victim = me->GetVictim()) + me->GetMotionMaster()->MoveChase(victim); + scheduler.Schedule(2s, 5s, [this](TaskContext context) + { + DoCastVictim(SPELL_SINISTER_STRIKE); + context.Repeat(7s, 8s); + }); + } + + void ScheduleOOC() + { + scheduler.Schedule(45s, [this](TaskContext context) + { + DoCastAOE(SPELL_FELBLOOD_CHANNEL); + context.Repeat(); + }); + } + + void UpdateAI(uint32 diff) override + { + scheduler.Update(diff); + + if (!me->IsCombatMovementAllowed() || !UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } +private: + ObjectGuid _protectorGUID; +}; + void AddSC_instance_sunwell_plateau() { new instance_sunwell_plateau(); RegisterSpellScript(spell_cataclysm_breath); + RegisterSunwellPlateauCreatureAI(npc_sunblade_scout); } diff --git a/src/server/scripts/OutdoorPvP/OutdoorPvPZM.cpp b/src/server/scripts/OutdoorPvP/OutdoorPvPZM.cpp index 0e9c367597c247..48a370737846bd 100644 --- a/src/server/scripts/OutdoorPvP/OutdoorPvPZM.cpp +++ b/src/server/scripts/OutdoorPvP/OutdoorPvPZM.cpp @@ -338,8 +338,7 @@ void OPvPCapturePointZM_Graveyard::SetBeaconState(TeamId controlling_factionId) bool OPvPCapturePointZM_Graveyard::CanTalkTo(Player* player, Creature* c, GossipMenuItems const& /*gso*/) { - ObjectGuid guid = c->GetGUID(); - auto itr = _creatureTypes.find(guid.GetCounter()); + auto itr = _creatureTypes.find(c->GetSpawnId()); if (itr != _creatureTypes.end()) { if (itr->second == ZM_ALLIANCE_FIELD_SCOUT && player->GetTeamId() == TEAM_ALLIANCE && m_BothControllingFactionId == TEAM_ALLIANCE && !m_FlagCarrierGUID && m_GraveyardState != ZM_GRAVEYARD_A) diff --git a/src/server/scripts/World/server_mail.cpp b/src/server/scripts/World/server_mail.cpp index 0e15f658b4d2d8..07272cd20e9718 100644 --- a/src/server/scripts/World/server_mail.cpp +++ b/src/server/scripts/World/server_mail.cpp @@ -16,10 +16,10 @@ */ #include "CreatureScript.h" -#include "ObjectMgr.h" #include "Player.h" #include "PlayerScript.h" #include "QueryResult.h" +#include "ServerMailMgr.h" class ServerMailReward : public PlayerScript { @@ -30,13 +30,14 @@ class ServerMailReward : public PlayerScript void OnPlayerLogin(Player* player) override { // Retrieve all server mail records and session only once - auto const& serverMailStore = sObjectMgr->GetAllServerMailStore(); + auto const& serverMailStore = sServerMailMgr->GetAllServerMailStore(); WorldSession* session = player->GetSession(); // We should always have a session, just incase if (!session) return; uint32 playerGUID = player->GetGUID().GetCounter(); + bool isAlliance = player->GetTeamId() == TEAM_ALLIANCE; for (auto const& [mailId, servMail] : serverMailStore) { @@ -45,26 +46,24 @@ class ServerMailReward : public PlayerScript stmt->SetData(1, mailId); // Capture servMail by value - auto callback = [session, servMailWrapper = std::reference_wrapper(servMail)](PreparedQueryResult result) + auto callback = [session, servMailWrapper = std::reference_wrapper(servMail), isAlliance](PreparedQueryResult result) { - ServerMail const& servMail = servMailWrapper.get(); // Dereference the wrapper to get the original object + ServerMail const& servMail = servMailWrapper.get(); // Dereference the wrapper to get the original object if (!result) { - sObjectMgr->SendServerMail( + uint32 money = isAlliance ? servMail.moneyA : servMail.moneyH; + std::vector const& items = isAlliance ? servMail.itemsA : servMail.itemsH; + std::vector const& conditions = servMail.conditions; + + sServerMailMgr->SendServerMail( session->GetPlayer(), servMail.id, - servMail.reqLevel, - servMail.reqPlayTime, - servMail.moneyA, - servMail.moneyH, - servMail.itemA, - servMail.itemCountA, - servMail.itemH, - servMail.itemCountH, + money, + items, + conditions, servMail.subject, - servMail.body, - servMail.active + servMail.body ); } };