diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 829a96197..f70764724 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -424,7 +424,7 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHAR_SOCIAL, "SELECT DISTINCT guid FROM character_social WHERE friend = ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHAR_OLD_CHARS, "SELECT guid, deleteInfos_Account FROM characters WHERE deleteDate IS NOT NULL AND deleteDate < ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_ARENA_TEAM_ID_BY_PLAYER_GUID, "SELECT arena_team_member.arenateamid FROM arena_team_member JOIN arena_team ON arena_team_member.arenateamid = arena_team.arenateamid WHERE guid = ? AND type = ? LIMIT 1", CONNECTION_SYNCH); - PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId FROM mail WHERE receiver = ? ORDER BY id DESC", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId, has_items FROM mail WHERE receiver = ? ORDER BY id DESC", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_NEXT_MAIL_DELIVERYTIME, "SELECT MIN(deliver_time) FROM mail WHERE receiver = ? AND deliver_time > ? AND (checked & 1) = 0 LIMIT 1", CONNECTION_SYNCH); PrepareStatement(CHAR_DEL_CHAR_AURA_FROZEN, "DELETE FROM character_aura WHERE spell = 9454 AND guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHAR_INVENTORY_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM character_inventory ci INNER JOIN item_instance ii ON ii.guid = ci.item WHERE itemEntry = ?", CONNECTION_SYNCH); diff --git a/src/server/game/Cache/CharacterCache.cpp b/src/server/game/Cache/CharacterCache.cpp index f940173dc..1840c5749 100644 --- a/src/server/game/Cache/CharacterCache.cpp +++ b/src/server/game/Cache/CharacterCache.cpp @@ -19,9 +19,12 @@ #include "ArenaTeam.h" #include "DatabaseEnv.h" #include "Log.h" +#include "MailMgr.h" #include "Player.h" #include "Timer.h" #include "World.h" +#include +#include #include namespace @@ -76,15 +79,7 @@ void CharacterCache::LoadCharacterCacheStorage() fields[4].Get() /*gender*/, fields[3].Get() /*race*/, fields[5].Get() /*class*/, fields[6].Get() /*level*/); } while (result->NextRow()); - QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail GROUP BY receiver"); - if (mailCountResult) - { - do - { - Field* fields = mailCountResult->Fetch(); - UpdateCharacterMailCount(ObjectGuid(HighGuid::Player, fields[0].Get()), static_cast(fields[1].Get()), true); - } while (mailCountResult->NextRow()); - } + sMailMgr->LoadMailCounts(); LOG_INFO("server.loading", ">> Loaded Character Infos For {} Characters in {} ms", _characterCacheStore.size(), GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", " "); @@ -105,15 +100,7 @@ void CharacterCache::RefreshCacheEntry(uint32 lowGuid) AddCharacterCacheEntry(ObjectGuid::Create(fields[0].Get()) /*guid*/, fields[2].Get() /*account*/, fields[1].Get() /*name*/, fields[4].Get() /*gender*/, fields[3].Get() /*race*/, fields[5].Get() /*class*/, fields[6].Get() /*level*/); } while (result->NextRow()); - QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail WHERE receiver = {} GROUP BY receiver", lowGuid); - if (mailCountResult) - { - do - { - Field* fields = mailCountResult->Fetch(); - UpdateCharacterMailCount(ObjectGuid(HighGuid::Player, fields[0].Get()), static_cast(fields[1].Get()), true); - } while (mailCountResult->NextRow()); - } + sMailMgr->RecountMailCount(lowGuid); } /* @@ -216,27 +203,25 @@ void CharacterCache::UpdateCharacterArenaTeamId(ObjectGuid const& guid, uint8 sl itr->second.ArenaTeamId[slot] = arenaTeamId; } -void CharacterCache::UpdateCharacterMailCount(ObjectGuid const& guid, int8 count, bool update) +void CharacterCache::UpdateCharacterMailCount(ObjectGuid const& guid, int32 count, bool update) { auto itr = _characterCacheStore.find(guid); if (itr == _characterCacheStore.end()) - { return; - } + + constexpr int32 maxCount = std::numeric_limits::max(); if (update) { - itr->second.MailCount = count; + itr->second.MailCount = static_cast(std::clamp(count, 0, maxCount)); return; } - // Let's be safe and prevent overflow - if (!itr->second.MailCount && count < 0) - { - return; - } + int32 newCount = static_cast(itr->second.MailCount) + count; + if (newCount < 0) + LOG_WARN("entities.player", "CharacterCache::UpdateCharacterMailCount: mail count for {} would go negative ({}), a mail insert was not reported; clamping to 0", guid.ToString(), newCount); - itr->second.MailCount += count; + itr->second.MailCount = static_cast(std::clamp(newCount, 0, maxCount)); } void CharacterCache::UpdateCharacterGroup(ObjectGuid const& guid, ObjectGuid groupGUID) diff --git a/src/server/game/Cache/CharacterCache.h b/src/server/game/Cache/CharacterCache.h index c0f8572ac..5ef83df06 100644 --- a/src/server/game/Cache/CharacterCache.h +++ b/src/server/game/Cache/CharacterCache.h @@ -33,7 +33,7 @@ struct CharacterCacheEntry uint8 Race; uint8 Sex; uint8 Level; - uint8 MailCount; + uint16 MailCount; ObjectGuid::LowType GuildId; std::array ArenaTeamId; ObjectGuid GroupGuid; @@ -58,10 +58,6 @@ class AC_GAME_API CharacterCache void UpdateCharacterGuildId(ObjectGuid const& guid, ObjectGuid::LowType guildId); void UpdateCharacterArenaTeamId(ObjectGuid const& guid, uint8 slot, uint32 arenaTeamId); - void UpdateCharacterMailCount(ObjectGuid const& guid, int8 count, bool update = false); - void DecreaseCharacterMailCount(ObjectGuid const& guid) { UpdateCharacterMailCount(guid, -1); }; - void IncreaseCharacterMailCount(ObjectGuid const& guid) { UpdateCharacterMailCount(guid, 1); }; - [[nodiscard]] bool HasCharacterCacheEntry(ObjectGuid const& guid) const; [[nodiscard]] CharacterCacheEntry const* GetCharacterCacheByGuid(ObjectGuid const& guid) const; [[nodiscard]] CharacterCacheEntry const* GetCharacterCacheByName(std::string const& name) const; @@ -78,6 +74,15 @@ class AC_GAME_API CharacterCache [[nodiscard]] ObjectGuid::LowType GetCharacterGuildIdByGuid(ObjectGuid guid) const; [[nodiscard]] uint32 GetCharacterArenaTeamIdByGuid(ObjectGuid guid, uint8 type) const; [[nodiscard]] ObjectGuid GetCharacterGroupGuidByGuid(ObjectGuid guid) const; + + private: + // Only MailMgr may touch the mail count, so every change is paired + // with the matching write to the `mail` table + void UpdateCharacterMailCount(ObjectGuid const& guid, int32 count, bool update = false); + void DecreaseCharacterMailCount(ObjectGuid const& guid) { UpdateCharacterMailCount(guid, -1); } + void IncreaseCharacterMailCount(ObjectGuid const& guid) { UpdateCharacterMailCount(guid, 1); } + + friend class MailMgr; }; #define sCharacterCache CharacterCache::instance() diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 0bc0ad0fe..8def7f56d 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -40,6 +40,7 @@ #include "Language.h" #include "Log.h" #include "LootItemStorage.h" +#include "MailMgr.h" #include "MapMgr.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" @@ -6298,10 +6299,21 @@ void Player::_LoadMail(PreparedQueryResult mailsResult, PreparedQueryResult mail m->checked = fields[10].Get(); m->stationery = fields[11].Get(); m->mailTemplateId = fields[12].Get(); + bool has_items = fields[13].Get(); if (cur_time > m->expire_time) { - LOG_DEBUG("entities.player", "Player::_LoadMail: Mail ({}) has expired - ignored.", m->messageID); + // Drop empty expired mail now; mail with items or money is left + // for ReturnOrDeleteOldMails, which owns return-to-sender handling + if (!has_items && !m->money && !m->COD) + { + LOG_DEBUG("entities.player", "Player::_LoadMail: Mail ({}) has expired - deleted.", m->messageID); + sMailMgr->DeleteEmptyExpiredMail(m->messageID, GetGUID().GetCounter()); + } + else + LOG_DEBUG("entities.player", "Player::_LoadMail: Mail ({}) has expired - ignored.", m->messageID); + + delete m; continue; } diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index f9c76d214..111dc7c26 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -6804,129 +6804,6 @@ void ObjectMgr::LoadNpcTextLocales() LOG_INFO("server.loading", ">> Loaded {} Npc Text Locale Strings in {} ms", (uint32)_npcTextLocaleStore.size(), GetMSTimeDiffToNow(oldMSTime)); } -void ObjectMgr::ReturnOrDeleteOldMails(bool serverUp) -{ - uint32 oldMSTime = getMSTime(); - - time_t curTime = GameTime::GetGameTime().count(); - - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_EXPIRED_MAIL); - stmt->SetData(0, uint32(curTime)); - PreparedQueryResult result = CharacterDatabase.Query(stmt); - if (!result) - return; - - std::map itemsCache; - stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_EXPIRED_MAIL_ITEMS); - stmt->SetData(0, uint32(curTime)); - if (PreparedQueryResult items = CharacterDatabase.Query(stmt)) - { - MailItemInfo item; - do - { - Field* fields = items->Fetch(); - item.item_guid = fields[0].Get(); - item.item_template = fields[1].Get(); - uint32 mailId = fields[2].Get(); - itemsCache[mailId].push_back(item); - } while (items->NextRow()); - } - - uint32 deletedCount = 0; - uint32 returnedCount = 0; - do - { - Field* fields = result->Fetch(); - Mail* m = new Mail; - m->messageID = fields[0].Get(); - m->messageType = fields[1].Get(); - m->sender = fields[2].Get(); - m->receiver = fields[3].Get(); - bool has_items = fields[4].Get(); - m->expire_time = time_t(fields[5].Get()); - m->deliver_time = time_t(0); - m->stationery = fields[6].Get(); - m->checked = fields[7].Get(); - m->mailTemplateId = fields[8].Get(); - - Player* player = nullptr; - if (serverUp) - player = ObjectAccessor::FindPlayerByLowGUID(m->receiver); - - if (player) // don't modify mails of a logged in player - { - delete m; - continue; - } - - // Delete or return mail - if (has_items) - { - // read items from cache - m->items.swap(itemsCache[m->messageID]); - - // If it is mail from non-player, or if it's already return mail, it shouldn't be returned, but deleted - if (!m->IsSentByPlayer() || m->IsSentByGM() || (m->IsCODPayment() || m->IsReturnedMail())) - { - for (auto const& mailedItem : m->items) - { - stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); - stmt->SetData(0, mailedItem.item_guid); - CharacterDatabase.Execute(stmt); - } - - stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID); - stmt->SetData(0, m->messageID); - CharacterDatabase.Execute(stmt); - } - else - { - // Mail will be returned - stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_MAIL_RETURNED); - stmt->SetData(0, m->receiver); - stmt->SetData(1, m->sender); - stmt->SetData(2, uint32(curTime + 30 * DAY)); - stmt->SetData(3, uint32(curTime)); - stmt->SetData (4, uint8(MAIL_CHECK_MASK_RETURNED)); - stmt->SetData(5, m->messageID); - CharacterDatabase.Execute(stmt); - for (auto const& mailedItem : m->items) - { - // Update receiver in mail items for its proper delivery, and in instance_item for avoid lost item at sender delete - stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_MAIL_ITEM_RECEIVER); - stmt->SetData(0, m->sender); - stmt->SetData(1, mailedItem.item_guid); - CharacterDatabase.Execute(stmt); - - stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ITEM_OWNER); - stmt->SetData(0, m->sender); - stmt->SetData(1, mailedItem.item_guid); - CharacterDatabase.Execute(stmt); - } - - // xinef: update global data - sCharacterCache->IncreaseCharacterMailCount(ObjectGuid(HighGuid::Player, m->sender)); - sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, m->receiver)); - - delete m; - ++returnedCount; - continue; - } - } - - sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, m->receiver)); - - stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); - stmt->SetData(0, m->messageID); - CharacterDatabase.Execute(stmt); - delete m; - ++deletedCount; - } while (result->NextRow()); - - LOG_INFO("server.loading", ">> Processed {} expired mails: {} deleted and {} returned in {} ms", deletedCount + returnedCount, deletedCount, returnedCount, GetMSTimeDiffToNow(oldMSTime)); - LOG_INFO("server.loading", " "); -} - void ObjectMgr::LoadQuestAreaTriggers() { uint32 oldMSTime = getMSTime(); diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index c384ae2f3..24f837cb7 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -1129,8 +1129,6 @@ public: return itr != _fishingBaseForAreaStore.end() ? itr->second : 0; } - void ReturnOrDeleteOldMails(bool serverUp); - CreatureBaseStats const* GetCreatureBaseStats(uint8 level, uint8 unitClass); void SetHighestGuids(); diff --git a/src/server/game/Handlers/MailHandler.cpp b/src/server/game/Handlers/MailHandler.cpp index e44001d36..e70a9d94f 100644 --- a/src/server/game/Handlers/MailHandler.cpp +++ b/src/server/game/Handlers/MailHandler.cpp @@ -26,6 +26,7 @@ #include "Language.h" #include "Log.h" #include "Mail.h" +#include "MailMgr.h" #include "ObjectMgr.h" #include "Opcodes.h" #include "Player.h" @@ -416,7 +417,7 @@ void WorldSession::HandleMailDelete(WorldPacket& recvData) Mail* m = _player->GetMail(mailId); Player* player = _player; player->m_mailsUpdated = true; - if (m) + if (m && m->state != MAIL_STATE_DELETED) { // delete shouldn't show up for COD mails if (m->COD) @@ -427,7 +428,7 @@ void WorldSession::HandleMailDelete(WorldPacket& recvData) m->state = MAIL_STATE_DELETED; - sCharacterCache->DecreaseCharacterMailCount(player->GetGUID()); + sMailMgr->OnMailDeleted(player->GetGUID().GetCounter()); } player->SendMailResult(mailId, MAIL_DELETED, MAIL_OK); } @@ -509,7 +510,7 @@ void WorldSession::HandleMailReturnToSender(WorldPacket& recvData) delete m; //we can deallocate old mail player->SendMailResult(mailId, MAIL_RETURNED_TO_SENDER, MAIL_OK); - sCharacterCache->DecreaseCharacterMailCount(player->GetGUID()); + sMailMgr->OnMailDeleted(player->GetGUID().GetCounter()); } //called when player takes item attached in mail diff --git a/src/server/game/Mails/Mail.cpp b/src/server/game/Mails/Mail.cpp index 81bc4085f..58051dcdb 100644 --- a/src/server/game/Mails/Mail.cpp +++ b/src/server/game/Mails/Mail.cpp @@ -24,6 +24,7 @@ #include "GameTime.h" #include "Item.h" #include "Log.h" +#include "MailMgr.h" #include "ObjectMgr.h" #include "Player.h" #include "ScriptMgr.h" @@ -251,7 +252,7 @@ void MailDraft::SendMailTo(CharacterDatabaseTransaction trans, MailReceiver cons trans->Append(stmt); } - sCharacterCache->IncreaseCharacterMailCount(ObjectGuid(HighGuid::Player, receiver.GetPlayerGUIDLow())); + sMailMgr->OnMailSent(receiver.GetPlayerGUIDLow()); // For online receiver update in game mail status and data if (pReceiver) diff --git a/src/server/game/Mails/MailMgr.cpp b/src/server/game/Mails/MailMgr.cpp new file mode 100644 index 000000000..3bb5278fb --- /dev/null +++ b/src/server/game/Mails/MailMgr.cpp @@ -0,0 +1,209 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "MailMgr.h" +#include "CharacterCache.h" +#include "Common.h" +#include "DatabaseEnv.h" +#include "GameTime.h" +#include "Log.h" +#include "Mail.h" +#include "ObjectAccessor.h" +#include "Timer.h" +#include + +MailMgr* MailMgr::instance() +{ + static MailMgr instance; + return &instance; +} + +void MailMgr::OnMailSent(ObjectGuid::LowType receiverLow) +{ + sCharacterCache->IncreaseCharacterMailCount(ObjectGuid(HighGuid::Player, receiverLow)); +} + +void MailMgr::OnMailDeleted(ObjectGuid::LowType receiverLow) +{ + sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, receiverLow)); +} + +void MailMgr::OnMailReturned(ObjectGuid::LowType oldReceiverLow, ObjectGuid::LowType newReceiverLow) +{ + sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, oldReceiverLow)); + sCharacterCache->IncreaseCharacterMailCount(ObjectGuid(HighGuid::Player, newReceiverLow)); +} + +void MailMgr::LoadMailCounts() +{ + QueryResult result = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail GROUP BY receiver"); + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + sCharacterCache->UpdateCharacterMailCount(ObjectGuid(HighGuid::Player, fields[0].Get()), static_cast(fields[1].Get()), true); + } while (result->NextRow()); +} + +void MailMgr::RecountMailCount(ObjectGuid::LowType receiverLow) +{ + int32 count = 0; + if (QueryResult result = CharacterDatabase.Query("SELECT COUNT(*) FROM mail WHERE receiver = {}", receiverLow)) + count = static_cast((*result)[0].Get()); + + sCharacterCache->UpdateCharacterMailCount(ObjectGuid(HighGuid::Player, receiverLow), count, true); +} + +void MailMgr::DeleteEmptyExpiredMail(uint32 mailId, ObjectGuid::LowType receiverLow) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); + stmt->SetData(0, mailId); + CharacterDatabase.Execute(stmt); + + OnMailDeleted(receiverLow); +} + +void MailMgr::ReturnOrDeleteOldMails(bool serverUp) +{ + uint32 oldMSTime = getMSTime(); + + time_t curTime = GameTime::GetGameTime().count(); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_EXPIRED_MAIL); + stmt->SetData(0, uint32(curTime)); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + if (!result) + return; + + std::map itemsCache; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_EXPIRED_MAIL_ITEMS); + stmt->SetData(0, uint32(curTime)); + if (PreparedQueryResult items = CharacterDatabase.Query(stmt)) + { + MailItemInfo item; + do + { + Field* fields = items->Fetch(); + item.item_guid = fields[0].Get(); + item.item_template = fields[1].Get(); + uint32 mailId = fields[2].Get(); + itemsCache[mailId].push_back(item); + } while (items->NextRow()); + } + + uint32 deletedCount = 0; + uint32 returnedCount = 0; + do + { + Field* fields = result->Fetch(); + Mail* m = new Mail; + m->messageID = fields[0].Get(); + m->messageType = fields[1].Get(); + m->sender = fields[2].Get(); + m->receiver = fields[3].Get(); + bool has_items = fields[4].Get(); + m->expire_time = time_t(fields[5].Get()); + m->deliver_time = time_t(0); + m->stationery = fields[6].Get(); + m->checked = fields[7].Get(); + m->mailTemplateId = fields[8].Get(); + + Player* player = nullptr; + if (serverUp) + player = ObjectAccessor::FindPlayerByLowGUID(m->receiver); + + if (player) // don't modify mails of a logged in player + { + delete m; + continue; + } + + // Keep each mail's correlated writes atomic + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + // Delete or return mail + if (has_items) + { + // read items from cache + m->items.swap(itemsCache[m->messageID]); + + // If it is mail from non-player, or if it's already return mail, it shouldn't be returned, but deleted + if (!m->IsSentByPlayer() || m->IsSentByGM() || (m->IsCODPayment() || m->IsReturnedMail())) + { + for (auto const& mailedItem : m->items) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); + stmt->SetData(0, mailedItem.item_guid); + trans->Append(stmt); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID); + stmt->SetData(0, m->messageID); + trans->Append(stmt); + } + else + { + // Mail will be returned + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_MAIL_RETURNED); + stmt->SetData(0, m->receiver); + stmt->SetData(1, m->sender); + stmt->SetData(2, uint32(curTime + 30 * DAY)); + stmt->SetData(3, uint32(curTime)); + stmt->SetData(4, uint8(MAIL_CHECK_MASK_RETURNED)); + stmt->SetData(5, m->messageID); + trans->Append(stmt); + for (auto const& mailedItem : m->items) + { + // Update receiver in mail items for its proper delivery, and in instance_item for avoid lost item at sender delete + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_MAIL_ITEM_RECEIVER); + stmt->SetData(0, m->sender); + stmt->SetData(1, mailedItem.item_guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ITEM_OWNER); + stmt->SetData(0, m->sender); + stmt->SetData(1, mailedItem.item_guid); + trans->Append(stmt); + } + + CharacterDatabase.CommitTransaction(trans); + + OnMailReturned(m->receiver, m->sender); + + delete m; + ++returnedCount; + continue; + } + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); + stmt->SetData(0, m->messageID); + trans->Append(stmt); + + CharacterDatabase.CommitTransaction(trans); + + OnMailDeleted(m->receiver); + + delete m; + ++deletedCount; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Processed {} expired mails: {} deleted and {} returned in {} ms", deletedCount + returnedCount, deletedCount, returnedCount, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} diff --git a/src/server/game/Mails/MailMgr.h b/src/server/game/Mails/MailMgr.h new file mode 100644 index 000000000..b8a9f69d8 --- /dev/null +++ b/src/server/game/Mails/MailMgr.h @@ -0,0 +1,87 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef _MAILMGR_H +#define _MAILMGR_H + +#include "Define.h" +#include "ObjectGuid.h" + +/** + * @brief Owns the mail lifecycle bookkeeping that lives outside a single player + * session: the per-character mail count mirrored in CharacterCache and the + * cleanup of expired mail. + * + * Every code path that inserts or deletes a row in the characters `mail` table + * must report it here, otherwise the cached count drifts until the next recount. + */ +class AC_GAME_API MailMgr +{ +public: + static MailMgr* instance(); + + /** + * @brief Reports a mail row inserted for a character. + * @param receiverLow Low GUID of the mail receiver + */ + void OnMailSent(ObjectGuid::LowType receiverLow); + + /** + * @brief Reports a mail row deleted from a character's mailbox. + * @param receiverLow Low GUID of the mail receiver + */ + void OnMailDeleted(ObjectGuid::LowType receiverLow); + + /** + * @brief Reports a mail row handed to a new receiver (return to sender). + * @param oldReceiverLow Low GUID of the previous receiver + * @param newReceiverLow Low GUID of the new receiver + */ + void OnMailReturned(ObjectGuid::LowType oldReceiverLow, ObjectGuid::LowType newReceiverLow); + + /** + * @brief Recounts the mail of all characters from the database. + * Called once at startup after the character cache is filled. + */ + void LoadMailCounts(); + + /** + * @brief Recounts one character's mail from the database, overwriting the + * cached value. + * @param receiverLow Low GUID of the character to recount + */ + void RecountMailCount(ObjectGuid::LowType receiverLow); + + /** + * @brief Deletes an expired mail row that has no items, money or COD + * attached. Used at login for mail that would otherwise stay invisible in + * the DB until ReturnOrDeleteOldMails catches the receiver offline. + * @param mailId Id of the mail row to delete + * @param receiverLow Low GUID of the mail receiver + */ + void DeleteEmptyExpiredMail(uint32 mailId, ObjectGuid::LowType receiverLow); + + /** + * @brief Returns expired mail with items to the sender and deletes the rest. + * @param serverUp When true, receivers that are currently online are skipped. + */ + void ReturnOrDeleteOldMails(bool serverUp); +}; + +#define sMailMgr MailMgr::instance() + +#endif diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index f2deafa83..f68c524c5 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -61,6 +61,7 @@ #include "LootItemStorage.h" #include "LootMgr.h" #include "M2Stores.h" +#include "MailMgr.h" #include "MapMgr.h" #include "Metric.h" #include "MotdMgr.h" @@ -847,7 +848,7 @@ void World::SetInitialWorldSettings() ///- Handle outdated emails (delete/return) LOG_INFO("server.loading", "Returning Old Mails..."); LOG_INFO("server.loading", " "); - sObjectMgr->ReturnOrDeleteOldMails(false); + sMailMgr->ReturnOrDeleteOldMails(false); ///- Load AutoBroadCast LOG_INFO("server.loading", "Loading Autobroadcasts..."); @@ -1196,7 +1197,7 @@ void World::Update(uint32 diff) if (currentGameTime > _mail_expire_check_timer) { - sObjectMgr->ReturnOrDeleteOldMails(true); + sMailMgr->ReturnOrDeleteOldMails(true); _mail_expire_check_timer = currentGameTime + 6h; } diff --git a/src/server/scripts/Commands/cs_cache.cpp b/src/server/scripts/Commands/cs_cache.cpp index 6f7899824..3c0866197 100644 --- a/src/server/scripts/Commands/cs_cache.cpp +++ b/src/server/scripts/Commands/cs_cache.cpp @@ -19,6 +19,7 @@ #include "CommandScript.h" #include "Group.h" #include "Language.h" +#include "MailMgr.h" #include "Player.h" #include "RBAC.h" @@ -122,7 +123,7 @@ public: sCharacterCache->UpdateCharacterAccountId(cPlayer->GetGUID(), cPlayer->GetSession()->GetAccountId()); sCharacterCache->UpdateCharacterGuildId(cPlayer->GetGUID(), cPlayer->GetGuildId()); - sCharacterCache->UpdateCharacterMailCount(cPlayer->GetGUID(), cPlayer->GetMailSize(), true); + sMailMgr->RecountMailCount(cPlayer->GetGUID().GetCounter()); sCharacterCache->UpdateCharacterArenaTeamId(cPlayer->GetGUID(), ARENA_SLOT_2v2, cPlayer->GetArenaTeamId(ARENA_SLOT_2v2)); sCharacterCache->UpdateCharacterArenaTeamId(cPlayer->GetGUID(), ARENA_SLOT_3v3, cPlayer->GetArenaTeamId(ARENA_SLOT_3v3)); sCharacterCache->UpdateCharacterArenaTeamId(cPlayer->GetGUID(), ARENA_SLOT_5v5, cPlayer->GetArenaTeamId(ARENA_SLOT_5v5)); diff --git a/src/server/scripts/Commands/cs_mail.cpp b/src/server/scripts/Commands/cs_mail.cpp index d8f09f85b..b646de392 100644 --- a/src/server/scripts/Commands/cs_mail.cpp +++ b/src/server/scripts/Commands/cs_mail.cpp @@ -23,6 +23,7 @@ #include "GameTime.h" #include "Language.h" #include "Mail.h" +#include "MailMgr.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" @@ -391,7 +392,7 @@ public: CharacterDatabase.CommitTransaction(trans); - sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, receiver)); + sMailMgr->OnMailDeleted(receiver); handler->PSendSysMessage(LANG_MAIL_RETURN_SUCCESS, mailId, handler->playerLink(target.GetName())); return true;