feat(Scripts/Commands): add .mail list and .mail return commands (#25213)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew 2026-03-26 06:59:05 -03:00 committed by GitHub
parent 35102dcfe9
commit fc2d3f332e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 422 additions and 1 deletions

View file

@ -1215,7 +1215,18 @@ enum AcoreStrings
LANG_AUTOBROADCAST_LOCALE_ENTRY = 5133,
LANG_AUTOBROADCAST_INVALID_LOCALE = 5134,
// Room for more strings 5135-9999
// Mail commands
LANG_MAIL_LIST_HEADER = 5135,
LANG_MAIL_LIST_ENTRY = 5136,
LANG_MAIL_LIST_EMPTY = 5137,
LANG_MAIL_RETURN_SUCCESS = 5138,
LANG_MAIL_RETURN_NOT_FOUND = 5139,
LANG_MAIL_RETURN_NOT_NORMAL = 5140,
LANG_MAIL_RETURN_NO_SENDER = 5141,
LANG_MAIL_RETURN_ALREADY_RETURNED = 5142,
LANG_MAIL_RETURN_HOOK_BLOCKED = 5143,
// Room for more strings 5144-9999
// Level requirement notifications
LANG_SAY_REQ = 6604,

View file

@ -0,0 +1,389 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Bag.h"
#include "CharacterCache.h"
#include "Chat.h"
#include "CommandScript.h"
#include "DatabaseEnv.h"
#include "GameTime.h"
#include "Language.h"
#include "Mail.h"
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "Timer.h"
using namespace Acore::ChatCommands;
class mail_commandscript : public CommandScript
{
public:
mail_commandscript() : CommandScript("mail_commandscript") { }
ChatCommandTable GetCommands() const override
{
static ChatCommandTable mailCommandTable =
{
{ "list", HandleMailListCommand, SEC_GAMEMASTER, Console::Yes },
{ "return", HandleMailReturnCommand, SEC_GAMEMASTER, Console::Yes }
};
static ChatCommandTable commandTable =
{
{ "mail", mailCommandTable }
};
return commandTable;
}
static char const* GetMailTypeString(uint8 messageType)
{
switch (messageType)
{
case MAIL_NORMAL: return "Normal";
case MAIL_AUCTION: return "Auction";
case MAIL_CREATURE: return "Creature";
case MAIL_GAMEOBJECT: return "GameObject";
case MAIL_CALENDAR: return "Calendar";
default: return "Unknown";
}
}
static char const* GetMailStationeryString(uint8 stationery)
{
switch (stationery)
{
case MAIL_STATIONERY_TEST: return "Test";
case MAIL_STATIONERY_DEFAULT: return "Default";
case MAIL_STATIONERY_GM: return "GM";
case MAIL_STATIONERY_AUCTION: return "Auction";
case MAIL_STATIONERY_VAL: return "Valentine";
case MAIL_STATIONERY_CHR: return "Christmas";
case MAIL_STATIONERY_ORP: return "Orphan";
default: return "Unknown";
}
}
static std::string GetItemListString(std::vector<MailItemInfo> const& items)
{
if (items.empty())
return "none";
std::string result;
for (size_t i = 0; i < items.size(); ++i)
{
if (i > 0)
result += ", ";
result += Acore::StringFormat("{}(guid:{})", items[i].item_template, items[i].item_guid);
}
return result;
}
static bool HandleMailListCommand(ChatHandler* handler, Optional<PlayerIdentifier> target)
{
if (!target)
target = PlayerIdentifier::FromTargetOrSelf(handler);
if (!target)
return false;
Player* player = target->GetConnectedPlayer();
if (player)
{
PlayerMails const& mails = player->GetMails();
if (mails.empty())
{
handler->PSendSysMessage(LANG_MAIL_LIST_EMPTY, handler->playerLink(target->GetName()));
return true;
}
handler->PSendSysMessage(LANG_MAIL_LIST_HEADER, handler->playerLink(target->GetName()));
for (Mail const* mail : mails)
{
std::string senderName;
if (mail->messageType == MAIL_NORMAL)
sCharacterCache->GetCharacterNameByGuid(ObjectGuid(HighGuid::Player, mail->sender), senderName);
std::string expireStr = Acore::Time::TimeToTimestampStr(Seconds(mail->expire_time));
std::string deliverStr = Acore::Time::TimeToTimestampStr(Seconds(mail->deliver_time));
handler->PSendSysMessage(LANG_MAIL_LIST_ENTRY,
mail->messageID, GetMailTypeString(mail->messageType), GetMailStationeryString(mail->stationery),
mail->mailTemplateId, mail->sender, senderName, mail->receiver,
expireStr, deliverStr, mail->money, mail->COD, mail->checked, GetItemListString(mail->items));
}
}
else
{
ObjectGuid::LowType lowGuid = target->GetGUID().GetCounter();
QueryResult result = CharacterDatabase.Query(
"SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId"
" FROM mail WHERE receiver = {} AND deliver_time <= {} ORDER BY id DESC",
lowGuid, GameTime::GetGameTime().count());
if (!result)
{
handler->PSendSysMessage(LANG_MAIL_LIST_EMPTY, handler->playerLink(target->GetName()));
return true;
}
handler->PSendSysMessage(LANG_MAIL_LIST_HEADER, handler->playerLink(target->GetName()));
do
{
Field* fields = result->Fetch();
uint32 messageID = fields[0].Get<uint32>();
uint8 messageType = fields[1].Get<uint8>();
uint32 sender = fields[2].Get<uint32>();
uint32 receiver = fields[3].Get<uint32>();
// fields[4] = subject (skipped)
// fields[5] = body (skipped)
uint32 expireTime = fields[6].Get<uint32>();
uint32 deliverTime = fields[7].Get<uint32>();
uint32 money = fields[8].Get<uint32>();
uint32 cod = fields[9].Get<uint32>();
uint32 checked = fields[10].Get<uint32>();
uint8 stationery = fields[11].Get<uint8>();
uint16 mailTemplate = fields[12].Get<uint16>();
std::string senderName;
if (messageType == MAIL_NORMAL)
sCharacterCache->GetCharacterNameByGuid(ObjectGuid(HighGuid::Player, sender), senderName);
std::string expireStr = Acore::Time::TimeToTimestampStr(Seconds(expireTime));
std::string deliverStr = Acore::Time::TimeToTimestampStr(Seconds(deliverTime));
// For offline players we don't have item details loaded
handler->PSendSysMessage(LANG_MAIL_LIST_ENTRY,
messageID, GetMailTypeString(messageType), GetMailStationeryString(stationery),
mailTemplate, sender, senderName, receiver,
expireStr, deliverStr, money, cod, checked, "N/A (offline)");
} while (result->NextRow());
}
return true;
}
static bool HandleMailReturnCommand(ChatHandler* handler, PlayerIdentifier target, uint32 mailId)
{
// Query mail data from DB so this works for offline players
QueryResult result = CharacterDatabase.Query(
"SELECT messageType, sender, receiver, subject, body, money, mailTemplateId, checked, deliver_time"
" FROM mail WHERE id = {}", mailId);
if (!result)
{
handler->SendErrorMessage(LANG_MAIL_RETURN_NOT_FOUND, mailId);
return true;
}
Field* fields = result->Fetch();
uint8 messageType = fields[0].Get<uint8>();
uint32 sender = fields[1].Get<uint32>();
uint32 receiver = fields[2].Get<uint32>();
std::string subject = fields[3].Get<std::string>();
std::string body = fields[4].Get<std::string>();
uint32 money = fields[5].Get<uint32>();
uint16 mailTemplate = fields[6].Get<uint16>();
uint32 checked = fields[7].Get<uint32>();
uint32 deliverTime = fields[8].Get<uint32>();
// Reject undelivered mail, same as the core handler
if (deliverTime > GameTime::GetGameTime().count())
{
handler->SendErrorMessage(LANG_MAIL_RETURN_NOT_FOUND, mailId);
return true;
}
// Verify the mail belongs to the target player
if (receiver != target.GetGUID().GetCounter())
{
handler->SendErrorMessage(LANG_MAIL_RETURN_NOT_FOUND, mailId);
return true;
}
if (messageType != MAIL_NORMAL)
{
handler->SendErrorMessage(LANG_MAIL_RETURN_NOT_NORMAL);
return true;
}
if (!sender)
{
handler->SendErrorMessage(LANG_MAIL_RETURN_NO_SENDER);
return true;
}
if (checked & MAIL_CHECK_MASK_RETURNED)
{
handler->SendErrorMessage(LANG_MAIL_RETURN_ALREADY_RETURNED);
return true;
}
Player* player = target.GetConnectedPlayer();
// Run the same script hook as the client return handler, failing early before any deletions
if (player)
{
Mail* m = player->GetMail(mailId);
if (m)
{
ObjectGuid senderGuid = ObjectGuid(HighGuid::Player, sender);
if (m->HasItems())
{
for (auto const& itemInfo : m->items)
{
Item* item = player->GetMItem(itemInfo.item_guid);
if (item && !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, item))
{
handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED);
return true;
}
}
}
else if (!sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, nullptr))
{
handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED);
return true;
}
}
}
// Same logic as WorldSession::HandleReturnToSender
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID);
stmt->SetData(0, mailId);
trans->Append(stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID);
stmt->SetData(0, mailId);
trans->Append(stmt);
MailDraft draft(subject, body);
if (mailTemplate)
draft = MailDraft(mailTemplate, false);
if (player)
{
// Online: same logic as WorldSession::HandleReturnToSender
// Get pointer before RemoveMail (which removes from deque but does not delete the object)
Mail* m = player->GetMail(mailId);
player->RemoveMail(mailId);
if (m && m->HasItems())
{
for (auto const& itemInfo : m->items)
{
if (Item* item = player->GetMItem(itemInfo.item_guid))
draft.AddItem(item);
player->RemoveMItem(itemInfo.item_guid);
}
}
delete m;
}
else
{
// Offline: load Item* objects from DB using same query shape as CHAR_SEL_MAILITEMS
// (LEFT JOIN to handle dangling mail_items) and same logic as Player::_LoadMailedItem
QueryResult itemResult = CharacterDatabase.Query(
"SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments,"
" randomPropertyId, durability, playedTime, text, mi.item_guid, itemEntry, ii.owner_guid"
" FROM mail_items mi LEFT JOIN item_instance ii ON mi.item_guid = ii.guid"
" WHERE mi.mail_id = {}", mailId);
if (itemResult)
{
do
{
Field* itemFields = itemResult->Fetch();
uint32 itemGuid = itemFields[11].Get<uint32>();
uint32 itemEntry = itemFields[12].Get<uint32>();
// Handle dangling mail_items (missing item_instance)
if (!itemEntry)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has dangling mail_items row for item_guid {}. Cleaning up.", mailId, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
continue;
}
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry);
if (!proto)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has unknown item (entry: {}, guid: {}). Cleaning up.", mailId, itemEntry, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
continue;
}
Item* item = NewItemOrBag(proto);
ObjectGuid ownerGuid = itemFields[13].Get<uint32>()
? ObjectGuid::Create<HighGuid::Player>(itemFields[13].Get<uint32>())
: ObjectGuid::Empty;
if (!item->LoadFromDB(itemGuid, ownerGuid, itemFields, itemEntry))
{
LOG_ERROR("misc", "cs_mail: Item (GUID: {}) in mail #{} failed to load. Cleaning up.", itemGuid, mailId);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
item->FSetState(ITEM_REMOVED);
CharacterDatabaseTransaction nullTrans = CharacterDatabaseTransaction(nullptr);
item->SaveToDB(nullTrans);
return true;
}
draft.AddItem(item);
} while (itemResult->NextRow());
}
}
uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(ObjectGuid(HighGuid::Player, receiver));
draft.AddMoney(money).SendReturnToSender(accountId, receiver, sender, trans);
CharacterDatabase.CommitTransaction(trans);
sCharacterCache->DecreaseCharacterMailCount(ObjectGuid(HighGuid::Player, receiver));
handler->PSendSysMessage(LANG_MAIL_RETURN_SUCCESS, mailId, handler->playerLink(target.GetName()));
return true;
}
};
void AddSC_mail_commandscript()
{
new mail_commandscript();
}

View file

@ -43,6 +43,7 @@ void AddSC_learn_commandscript();
void AddSC_lfg_commandscript();
void AddSC_list_commandscript();
void AddSC_lookup_commandscript();
void AddSC_mail_commandscript();
void AddSC_message_commandscript();
void AddSC_misc_commandscript();
void AddSC_mmaps_commandscript();
@ -98,6 +99,7 @@ void AddCommandsScripts()
AddSC_lfg_commandscript();
AddSC_list_commandscript();
AddSC_lookup_commandscript();
AddSC_mail_commandscript();
AddSC_message_commandscript();
AddSC_misc_commandscript();
AddSC_mmaps_commandscript();