From d43cdef1045e3b8343e21d689fd7e03a9ed5b774 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:38:39 -0500 Subject: [PATCH] feat(mail): notify online recipients of new mail on any world Mail sent to someone who is not in the sender's world (system mail with no address, and all player-to-player mail) now reaches them: the world sends a MailNotify (Chat::MAIL, unused until now) to the chat server, which passes it to the world the receiver is in, and that world sends the client its unread count with a NewMail NotificationResponse. Receivers in the same world are told directly. Player-to-player mail did not notify the receiver at all before. Replaces the TODO in Mail::SendMail. Co-Authored-By: Claude Opus 5.5 --- dChatServer/ChatServer.cpp | 8 +++++ dGame/dUtilities/Mail.cpp | 32 ++++++++++++++++++- dGame/dUtilities/Mail.h | 7 ++++ dNet/ChatPackets.cpp | 9 ++++++ dNet/ChatPackets.h | 9 ++++++ dWorldServer/WorldServer.cpp | 5 +++ .../dGameTests/dNetTests/ChatPacketsTests.cpp | 9 ++++++ 7 files changed, 78 insertions(+), 1 deletion(-) diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index d780fb910..bd0487fb1 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -269,6 +269,14 @@ namespace { handlers.On(Chat::WHO, ChatPacketHandler::HandleWho); handlers.On(Chat::SHOW_ALL, ChatPacketHandler::HandleShowAll); handlers.On(Chat::ACHIEVEMENT_NOTIFY, ChatPacketHandler::OnAchievementNotify); + // New mail: pass it on to the world the receiver is in, if they are online + handlers.On(Chat::MAIL, [](const MailNotify& notify, const SystemAddress&) { + const auto& receiver = Game::playerContainer.GetPlayerData(notify.receiverID); + if (!receiver) return; + RakNet::BitStream bitStream; + notify.WritePacket(bitStream); + Game::server->Send(bitStream, receiver.worldServerSysAddr, false); + }); return handlers; }(); return handlers; diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index 836ca1145..e7630ed6c 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -22,6 +22,8 @@ #include "Item.h" #include "MissionComponent.h" #include "ChatPackets.h" +#include "ChatServerLink.h" +#include "eObjectBits.h" #include "Character.h" #include "dZoneManager.h" #include "WorldConfig.h" @@ -149,6 +151,7 @@ namespace Mail { Database::Get()->InsertNewMail(mailInfo); DashboardNotify::Changed("mail", mailInfo.receiverId); + NotifyNewMail(mailInfo.receiverId); if (hasAttachment) { EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::MAIL_SENT, .itemId = sentItemId, .newItemId = mailInfo.itemID, .lot = mailInfo.itemLOT, .count = static_cast(mailInfo.itemCount), @@ -431,8 +434,35 @@ void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, LWOOBJ Database::Get()->InsertNewMail(mailInsert); DashboardNotify::Changed("mail", mailInsert.receiverId); - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return; // TODO: Echo to chat server + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) { + NotifyNewMail(recipient); + return; + } NotificationResponse response; response.status = eNotificationResponse::NewMail; response.Send(sysAddr); } + +bool Mail::NotifyNewMailHere(LWOOBJID receiver) { + // Mail stores the character ID; the player's object ID also carries the character bit + GeneralUtils::SetBit(receiver, eObjectBits::CHARACTER); + auto* const player = Game::entityManager->GetEntity(receiver); + if (!player || !player->IsPlayer()) return false; + auto* const character = player->GetCharacter(); + if (!character) return false; + + NotificationResponse response; + response.status = eNotificationResponse::NewMail; + response.mailCount = Database::Get()->GetUnreadMailCount(character->GetID()); + response.Send(player->GetSystemAddress()); + return true; +} + +void Mail::NotifyNewMail(const LWOOBJID receiver) { + if (NotifyNewMailHere(receiver)) return; + + ChatPackets::MailNotify notify; + notify.receiverID = receiver; + GeneralUtils::SetBit(notify.receiverID, eObjectBits::CHARACTER); + ChatServerLink::Send(notify, MEDIUM_PRIORITY); +} diff --git a/dGame/dUtilities/Mail.h b/dGame/dUtilities/Mail.h index 9c83025f8..3fdc8084c 100644 --- a/dGame/dUtilities/Mail.h +++ b/dGame/dUtilities/Mail.h @@ -210,6 +210,13 @@ namespace Mail { void HandleMail(RakNet::BitStream& inStream, const SystemAddress& sysAddr, Entity* player); + // Tells the receiver about their unread mail if they are a player in this world. Returns false if they are not. + bool NotifyNewMailHere(LWOOBJID receiver); + + // Tells the receiver about their new mail wherever they are online: in this world directly, otherwise through the + // chat server to the world they are in. + void NotifyNewMail(LWOOBJID receiver); + void SendMail( const Entity* recipient, const std::string& subject, diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index 224e4e21c..7906354e8 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -168,6 +168,15 @@ namespace ChatPackets { return true; } + void MailNotify::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(receiverID); + } + + bool MailNotify::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(receiverID)); + return true; + } + void ShowAllRequest::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(requestor); bitStream.Write(displayZoneData); diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index 90a732f82..41fd15420 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -126,6 +126,15 @@ namespace ChatPackets { bool Deserialize(RakNet::BitStream& bitStream) override; }; + // World -> chat -> world: receiverID has new mail. DLU's own server packet: the chat server passes it on to the world + // the receiver is in, which sends them the new-mail notification. Characters that are offline are skipped. + struct MailNotify : public LUBitStream { + LWOOBJID receiverID{ LWOOBJID_EMPTY }; + MailNotify() : LUBitStream(ServiceType::CHAT, MessageType::Chat::MAIL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + // World -> chat: /showall struct ShowAllRequest : public LUBitStream { LWOOBJID requestor = LWOOBJID_EMPTY; diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 396fc8f42..5f5ef1018 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -623,6 +623,11 @@ namespace { Game::server->Send(bitStream, sysAddr, false); //send routed packet to player }); + // New mail for a player the chat server says is in this world + handlers.On(MessageType::Chat::MAIL, [](const ChatPackets::MailNotify& notify, const SystemAddress&) { + Mail::NotifyNewMailHere(notify.receiverID); + }); + handlers.On(MessageType::Chat::GM_ANNOUNCE, [](const ChatPackets::Announcement& announcement, const SystemAddress&) { //Send to our clients: AMFArrayValue args; diff --git a/tests/dGameTests/dNetTests/ChatPacketsTests.cpp b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp index 4cf147037..66b9fd960 100644 --- a/tests/dGameTests/dNetTests/ChatPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp @@ -898,3 +898,12 @@ TEST_F(ChatPacketsTests, AchievementNotifyRoundTrips) { EXPECT_EQ(copy.targetPlayerName.string, u"Friend"); ExpectTruncatedFails(notify); } + +// DLU's own world -> chat -> world new-mail notice: header (chat service, MAIL) and the receiver's object ID. +TEST_F(ChatPacketsTests, MailNotifyRoundTrips) { + ChatPackets::MailNotify notify; + notify.receiverID = 0x1000000000000042LL; + EXPECT_PACKET_EQ(FromHex("53 02 00 24 00 00 00 00 42 00 00 00 00 00 00 10"), StructPacket(notify)); + EXPECT_EQ(RoundTrip(notify).receiverID, notify.receiverID); + ExpectTruncatedFails(notify); +}