From 231096132c5046ee446187702bce1f9fbb8579c7 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:18:01 +0100 Subject: [PATCH] refactor(Core/ServerMail): Refactor to Dedicated Manager Class with Multi-Item & Condition Support (#21590) 1. Core Cleanup - Move all ServerMail logic from `ObjectMgr` into a new dedicated `ServerMailMgr` class - Move faction logic for money from SendServerMail into the script 2. Separation of items into a new table - Create a new `mail_server_template_items` table - Allows to send multiple items in one mail - Separate items per faction Alliance/Horde 3. Separation of conditions into a new table - Create a new `mail_server_template_conditions` table - Allows to use multiple conditions for one mail - Available condition types - Minimum playtime (playerLevel >= condition) - Minimum playtime (playerPlayTime >= condition) - Rewarded quest - Earned achievement - Earned reputation (playerReputation >= conditionState) - Faction - Race - Class 4. Updated ServerMail loading - Move item and condition loading to their own functions - LoadMailServerTemplateItems() - LoadMailServerTemplateConditions() 5. Reworked eligibility check - Player needs to pass all conditions to be eligible for the mail - All players are automatically eligible if no conditions exist for a server mail template. 6. Updated foreign keys - For table `mail_server_character`, `mail_server_template_conditions`, `mail_server_template_items` foreign key with on delete cascade is added for automatic removal of entries if mail_server_template.id is deleted. 7. Database changes - See the PR --- .../rev_1740310084140595600.sql | 65 ++++ src/server/game/Globals/ObjectMgr.cpp | 113 ------ src/server/game/Globals/ObjectMgr.h | 7 - src/server/game/Mails/Mail.h | 17 - src/server/game/Mails/ServerMailMgr.cpp | 351 ++++++++++++++++++ src/server/game/Mails/ServerMailMgr.h | 231 ++++++++++++ src/server/game/World/World.cpp | 5 +- src/server/scripts/Commands/cs_reload.cpp | 3 +- src/server/scripts/World/server_mail.cpp | 29 +- 9 files changed, 666 insertions(+), 155 deletions(-) create mode 100644 data/sql/updates/pending_db_characters/rev_1740310084140595600.sql create mode 100644 src/server/game/Mails/ServerMailMgr.cpp create mode 100644 src/server/game/Mails/ServerMailMgr.h diff --git a/data/sql/updates/pending_db_characters/rev_1740310084140595600.sql b/data/sql/updates/pending_db_characters/rev_1740310084140595600.sql new file mode 100644 index 000000000..aa8a06ac6 --- /dev/null +++ b/data/sql/updates/pending_db_characters/rev_1740310084140595600.sql @@ -0,0 +1,65 @@ +-- +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/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 47cdd1cf7..a4033ca1c 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 553bf3b26..8826ce801 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 @@ public: void LoadInstanceTemplate(); void LoadInstanceEncounters(); void LoadMailLevelRewards(); - void LoadMailServerTemplates(); void LoadVehicleTemplateAccessories(); void LoadVehicleAccessories(); void LoadVehicleSeatAddon(); @@ -1188,8 +1186,6 @@ public: 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 @@ public: } [[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 @@ private: 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 17c078e25..f89a5e127 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 000000000..d11595066 --- /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 000000000..9cf70a805 --- /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/World/World.cpp b/src/server/game/World/World.cpp index 097d4f655..f7d415b88 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 45434d502..8793cebd2 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 @@ public: 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/World/server_mail.cpp b/src/server/scripts/World/server_mail.cpp index 0e15f658b..07272cd20 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 @@ public: 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 @@ public: 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 ); } };