diff --git a/data/sql/updates/pending_db_characters/rev_1693343760497114600.sql b/data/sql/updates/pending_db_characters/rev_1693343760497114600.sql new file mode 100644 index 00000000..30fd8477 --- /dev/null +++ b/data/sql/updates/pending_db_characters/rev_1693343760497114600.sql @@ -0,0 +1,23 @@ +DROP TABLE IF EXISTS `mail_server_character`; +CREATE TABLE IF NOT EXISTS `mail_server_character` ( + `guid` INT UNSIGNED NOT NULL, + `mailId` INT UNSIGNED NOT NULL, + PRIMARY KEY (`guid`, `mailId`) +) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4; + +DROP TABLE IF EXISTS `mail_server_template`; +CREATE TABLE IF NOT EXISTS `mail_server_template` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `reqLevel` TINYINT UNSIGNED NOT NULL DEFAULT '0', + `reqPlayTime` INT UNSIGNED NOT NULL DEFAULT '0', + `moneyA` INT UNSIGNED NOT NULL DEFAULT '0', + `moneyH` INT UNSIGNED NOT NULL DEFAULT '0', + `itemA` INT UNSIGNED NOT NULL DEFAULT '0', + `itemCountA` INT UNSIGNED NOT NULL DEFAULT '0', + `itemH` INT UNSIGNED NOT NULL DEFAULT '0', + `itemCountH` INT UNSIGNED NOT NULL DEFAULT '0', + `subject` TEXT NOT NULL, + `body` TEXT NOT NULL, + `active` TINYINT UNSIGNED NOT NULL DEFAULT '1', + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4; diff --git a/data/sql/updates/pending_db_world/rev_1693344185076263500.sql b/data/sql/updates/pending_db_world/rev_1693344185076263500.sql new file mode 100644 index 00000000..d60157a0 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1693344185076263500.sql @@ -0,0 +1,3 @@ +DELETE FROM `command` WHERE `name`='reload mail_server_template'; +INSERT INTO `command` (`name`, `help`) VALUES +('reload mail_server_template', 'Syntax: .reload mail_server_template\nReload server_mail_template table.'); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 7510b3bc..bbb7f66a 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -121,6 +121,8 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHARACTER_ACTIONS, "SELECT a.button, a.action, a.type FROM character_action as a, characters as c WHERE a.guid = c.guid AND a.spec = c.activeTalentGroup AND a.guid = ? ORDER BY button", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_MAIL_COUNT, "SELECT COUNT(*) FROM mail WHERE receiver = ?", CONNECTION_SYNCH); + PrepareStatement(CHAR_SEL_MAIL_SERVER_CHARACTER, "SELECT mailId from mail_server_character WHERE guid = ? and mailId = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_REP_MAIL_SERVER_CHARACTER, "REPLACE INTO mail_server_character (guid, mailId) values (?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_SOCIALLIST, "SELECT friend, flags, note FROM character_social JOIN characters ON characters.guid = character_social.friend WHERE character_social.guid = ? AND deleteinfos_name IS NULL LIMIT 255", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index a8a2cd76..79406937 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -93,6 +93,8 @@ enum CharacterDatabaseStatements : uint32 CHAR_SEL_CHARACTER_ACTIONS, CHAR_SEL_CHARACTER_ACTIONS_SPEC, CHAR_SEL_MAIL_COUNT, + CHAR_SEL_MAIL_SERVER_CHARACTER, + CHAR_REP_MAIL_SERVER_CHARACTER, CHAR_SEL_CHARACTER_SOCIALLIST, CHAR_SEL_CHARACTER_HOMEBIND, CHAR_SEL_CHARACTER_SPELLCOOLDOWNS, diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 37943f25..7aca1038 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -3164,6 +3164,121 @@ void ObjectMgr::LoadVehicleAccessories() LOG_INFO("server.loading", ">> Loaded %u Vehicle Accessories in %u ms", count, GetMSTimeDiffToNow(oldMSTime)); } +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->setUInt32(0, player->GetGUID().GetCounter()); + stmt->setUInt32(1, id); + CharacterDatabase.Execute(stmt); + + LOG_DEBUG("entities.player", "ObjectMgr::SendServerMail() Sent mail id %u to %s", id, player->GetGUID().ToString().c_str()); + } +} + +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."); + return; + } + + _serverMailStore.rehash(result->GetRowCount()); + + do + { + Field* fields = result->Fetch(); + + uint32 id = fields[0].GetUInt32(); + + ServerMail& servMail = _serverMailStore[id]; + + servMail.id = id; + servMail.reqLevel = fields[1].GetUInt8(); + servMail.reqPlayTime = fields[2].GetUInt32(); + servMail.moneyA = fields[3].GetUInt32(); + servMail.moneyH = fields[4].GetUInt32(); + servMail.itemA = fields[5].GetUInt32(); + servMail.itemCountA = fields[6].GetUInt32(); + servMail.itemH = fields[7].GetUInt32(); + servMail.itemCountH = fields[8].GetUInt32(); + servMail.subject = fields[9].GetString(); + servMail.body = fields[10].GetString(); + servMail.active = fields[11].GetUInt8(); + + if (servMail.reqLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + { + LOG_ERROR( + "sql.sql", "Table `mail_server_template` has reqLevel %u but max level is %u for id %u, 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 %u or moneyH %u larger than MAX_MONEY_AMOUNT %u for id %u, 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 %u for id %u, 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 %u for id %u, skipped.", servMail.itemH, servMail.id); + return; + } + + if (!servMail.itemA && servMail.itemCountA) + { + LOG_ERROR("sql.sql", "Table `mail_server_template` has itemCountA %u 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 %u with no ItemH, set to 0", servMail.itemCountH); + servMail.itemCountH = 0; + } + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded %lu Mail Server Template in %u ms", _serverMailStore.size(), GetMSTimeDiffToNow(oldMSTime)); +} + void ObjectMgr::LoadVehicleSeatAddon() { uint32 oldMSTime = getMSTime(); diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 5a877045..66d235db 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -870,7 +870,26 @@ struct QuestGreeting typedef std::unordered_map> QuestGreetingContainer; -struct GraveyardData +struct ServerMail +{ + ServerMail() = default; + uint32 id{}; + uint8 reqLevel{}; + uint32 reqPlayTime{}; + uint32 moneyA{}; + uint32 moneyH{}; + uint32 itemA{}; + uint32 itemCountA{}; + uint32 itemH{}; + uint32 itemCountH{}; + std::string subject; + std::string body; + uint8 active{}; +}; + +typedef std::unordered_map ServerMailContainer; + + struct GraveyardData { uint32 safeLocId; uint32 team; @@ -1217,6 +1236,7 @@ class FC_GAME_API ObjectMgr void LoadInstanceTemplate(); void LoadInstanceEncounters(); void LoadMailLevelRewards(); + void LoadMailServerTemplates(); void LoadVehicleTemplateAccessories(); void LoadVehicleAccessories(); void LoadVehicleSeatAddon(); @@ -1350,6 +1370,11 @@ class FC_GAME_API ObjectMgr return nullptr; } + ServerMailContainer const& GetAllServerMailStore() const + { + return _serverMailStore; + } + BroadcastText const* GetBroadcastText(uint32 id) const { BroadcastTextContainer::const_iterator itr = _broadcastTextStore.find(id); @@ -1603,6 +1628,9 @@ class FC_GAME_API ObjectMgr bool IsTransportMap(uint32 mapId) const { return _transportMaps.count(mapId) != 0; } + 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; + VehicleSeatAddon const* GetVehicleSeatAddon(uint32 seatId) const { VehicleSeatAddonContainer::const_iterator itr = _vehicleSeatAddonStore.find(seatId); @@ -1774,6 +1802,7 @@ class FC_GAME_API ObjectMgr FirelandsStringContainer _firelandsStringStore; CacheVendorItemContainer _cacheVendorItemStore; + ServerMailContainer _serverMailStore; std::unordered_map _trainers; std::map, uint32> _creatureDefaultTrainers; diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 574e3814..489cf21d 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -2064,7 +2064,11 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Player level dependent mail rewards..."); sObjectMgr->LoadMailLevelRewards(); + LOG_INFO("server.loading", "Loading Mail Server Template..."); // must be after load LoadMailLevelRewards + sObjectMgr->LoadMailServerTemplates(); + // Loot tables + LOG_INFO("server.loading", "Loading Loot Tables..."); LoadLootTables(); LOG_INFO("server.loading", "Loading Skill Discovery Table..."); diff --git a/src/server/scripts/Commands/cs_reload.cpp b/src/server/scripts/Commands/cs_reload.cpp index 4b5e630f..a8ecd801 100644 --- a/src/server/scripts/Commands/cs_reload.cpp +++ b/src/server/scripts/Commands/cs_reload.cpp @@ -119,6 +119,7 @@ class reload_commandscript : public CommandScript { "quest_template_locale", rbac::RBAC_PERM_COMMAND_RELOAD_QUEST_TEMPLATE_LOCALE, true, &HandleReloadLocalesQuestCommand, "" }, { "mail_level_reward", rbac::RBAC_PERM_COMMAND_RELOAD_MAIL_LEVEL_REWARD, true, &HandleReloadMailLevelRewardCommand, "" }, { "mail_loot_template", rbac::RBAC_PERM_COMMAND_RELOAD_MAIL_LOOT_TEMPLATE, true, &HandleReloadLootTemplatesMailCommand, "" }, + { "mail_server_template", rbac::RBAC_PERM_COMMAND_RELOAD_MAIL_LOOT_TEMPLATE, true, &HandleReloadMailServerTemplateCommand, "" }, { "milling_loot_template", rbac::RBAC_PERM_COMMAND_RELOAD_MILLING_LOOT_TEMPLATE, true, &HandleReloadLootTemplatesMillingCommand, "" }, { "npc_spellclick_spells", rbac::RBAC_PERM_COMMAND_RELOAD_NPC_SPELLCLICK_SPELLS, true, &HandleReloadSpellClickSpellsCommand, "" }, { "npc_vendor", rbac::RBAC_PERM_COMMAND_RELOAD_NPC_VENDOR, true, &HandleReloadNpcVendorCommand, "" }, @@ -652,6 +653,14 @@ class reload_commandscript : public CommandScript return true; } + static bool HandleReloadMailServerTemplateCommand(ChatHandler* handler, char const* /*args*/) + { + LOG_INFO("server.loading", "Re-Loading `server_mail_template` table"); + sObjectMgr->LoadMailServerTemplates(); + handler->SendGlobalGMSysMessage("DB table `server_mail_template` reloaded."); + return true; + } + static bool HandleReloadLootTemplatesReferenceCommand(ChatHandler* handler, char const* /*args*/) { LOG_INFO("misc", "Re-Loading Loot Tables LootType 10 (Reference)..."); diff --git a/src/server/scripts/World/server_mail.cpp b/src/server/scripts/World/server_mail.cpp new file mode 100644 index 00000000..465afcc8 --- /dev/null +++ b/src/server/scripts/World/server_mail.cpp @@ -0,0 +1,57 @@ +/* + * This file is part of the FirelandsCore 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 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 Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +#include "DatabaseEnv.h" +#include "ObjectMgr.h" +#include "Player.h" +#include "QueryResult.h" +#include "ScriptMgr.h" +#include "WorldSession.h" + +class ServerMailReward : public PlayerScript +{ + public: + ServerMailReward() : PlayerScript("ServerMailReward") {} + + // CHARACTER_LOGIN = 8 + void OnLogin(Player* player, bool /*firstLogin*/) + { + for (auto const& servMail : sObjectMgr->GetAllServerMailStore()) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL_SERVER_CHARACTER); + stmt->setUInt32(0, player->GetGUID().GetCounter()); + stmt->setUInt32(1, servMail.second.id); + + WorldSession* mySess = player->GetSession(); + mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt).WithPreparedCallback( + [mySess, servMail](PreparedQueryResult result) + { + if (!result) + { + sObjectMgr->SendServerMail(mySess->GetPlayer(), servMail.second.id, servMail.second.reqLevel, servMail.second.reqPlayTime, servMail.second.moneyA, servMail.second.moneyH, + servMail.second.itemA, servMail.second.itemCountA, servMail.second.itemH, servMail.second.itemCountH, servMail.second.subject, servMail.second.body, + servMail.second.active); + } + })); + } + } +}; + +void AddSC_server_mail() +{ + new ServerMailReward(); +} diff --git a/src/server/scripts/World/world_script_loader.cpp b/src/server/scripts/World/world_script_loader.cpp index f0a9b5a5..40663433 100644 --- a/src/server/scripts/World/world_script_loader.cpp +++ b/src/server/scripts/World/world_script_loader.cpp @@ -29,12 +29,12 @@ void AddSC_npc_innkeeper(); void AddSC_npcs_special(); void AddSC_achievement_scripts(); void AddSC_action_ip_logger(); +void AddSC_server_mail(); void AddSC_duel_reset(); void AddSC_world_map_scripts(); void AddSC_quest_scripts(); // player void AddSC_chat_log(); -void AddSC_action_ip_logger(); // The name of this function should match: // void Add${NameOfDirectory}Scripts() @@ -52,6 +52,7 @@ void AddWorldScripts() AddSC_world_map_scripts(); AddSC_quest_scripts(); AddSC_chat_log(); // location: scripts\World\chat_log.cpp + AddSC_server_mail(); // location: scripts\World\server_mail.cpp // FIXME: This should be moved in a script validation hook. // To avoid duplicate code, we check once /*ONLY*/ if logging is permitted or not.