diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index 6254954c1..e258824aa 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -181,10 +181,12 @@ namespace Mail { void NotificationResponse::Serialize(RakNet::BitStream& bitStream) const { MailLUBitStream::Serialize(bitStream); bitStream.Write(status); - bitStream.Write(0); // unused - bitStream.Write(0); // unused - bitStream.Write(auctionID); - bitStream.Write(0); // unused + bitStream.Write(mailID); + bitStream.Write(receiverID); + bitStream.Write(attachmentID); + bitStream.Write(attachmentLOT); + bitStream.Write(attachmentCount); + bitStream.Write(0); // packing bitStream.Write(mailCount); bitStream.Write(0); // packing } @@ -339,6 +341,7 @@ namespace Mail { if (character) { auto unreadMailCount = Database::Get()->GetUnreadMailCount(character->GetID()); response.status = eNotificationResponse::NewMail; + response.receiverID = player->GetObjectID(); response.mailCount = unreadMailCount; } @@ -418,7 +421,7 @@ void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, const void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, LWOOBJID recipient, const std::string& recipientName, const std::string& subject, const std::string& body, const LOT attachment, - const uint16_t attachmentCount, const SystemAddress& sysAddr) { + const uint16_t attachmentCount, [[maybe_unused]] const SystemAddress& sysAddr) { MailInfo mailInsert; mailInsert.senderUsername = senderName; mailInsert.recipient = recipientName; @@ -434,21 +437,32 @@ 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) { - NotifyNewMail(recipient); - return; - } - NotificationResponse response; - response.status = eNotificationResponse::NewMail; - response.Send(sysAddr); + NotifyNewMail(recipient); } -void Mail::NotifyUnreadMailOnLoad(const uint32_t unreadCount, const SystemAddress& sysAddr) { - if (unreadCount == 0) return; - NotificationResponse response; - response.status = eNotificationResponse::NewMail; - response.mailCount = unreadCount; - response.Send(sysAddr); +Mail::NotificationResponse Mail::NewMailNotice(const MailInfo& mail, const LWOOBJID playerID) { + NotificationResponse notice; + notice.status = eNotificationResponse::NewMail; + notice.mailID = mail.id; + notice.receiverID = playerID; + notice.attachmentID = mail.itemID; + notice.attachmentLOT = mail.itemLOT > 0 ? mail.itemLOT : LOT_NULL; + notice.attachmentCount = static_cast(mail.itemCount); + notice.mailCount = 1; + return notice; +} + +std::vector Mail::UnreadMailNotices(std::vector mail, const LWOOBJID playerID) { + std::sort(mail.begin(), mail.end(), [](const MailInfo& a, const MailInfo& b) { return a.id < b.id; }); + std::vector notices; + for (const auto& entry : mail) { + if (!entry.wasRead) notices.push_back(NewMailNotice(entry, playerID)); + } + return notices; +} + +void Mail::NotifyUnreadMailOnLoad(const LWOOBJID characterID, const LWOOBJID playerID, const SystemAddress& sysAddr) { + for (const auto& notice : UnreadMailNotices(Database::Get()->GetMailForPlayer(characterID, 20), playerID)) notice.Send(sysAddr); } bool Mail::NotifyNewMailHere(LWOOBJID receiver) { @@ -459,10 +473,13 @@ bool Mail::NotifyNewMailHere(LWOOBJID receiver) { 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()); + // About the newest unread mail, the one that just arrived + const auto mail = Database::Get()->GetMailForPlayer(character->GetID(), 20); + const MailInfo* newest = nullptr; + for (const auto& entry : mail) { + if (!entry.wasRead && (!newest || entry.id > newest->id)) newest = &entry; + } + if (newest) NewMailNotice(*newest, player->GetObjectID()).Send(player->GetSystemAddress()); return true; } diff --git a/dGame/dUtilities/Mail.h b/dGame/dUtilities/Mail.h index aaab9b4a9..5991046b7 100644 --- a/dGame/dUtilities/Mail.h +++ b/dGame/dUtilities/Mail.h @@ -2,6 +2,7 @@ #define __MAIL_H__ #include +#include #include "BitStream.h" #include "RakNetTypes.h" #include "dCommonVars.h" @@ -131,9 +132,15 @@ namespace Mail { void Serialize(RakNet::BitStream& bitStream) const override; }; + // Live filled a NewMail notice with the mail it was about (the answer to NotificationRequest named no mail and + // carried the unread count). The client reads the status and mailCount, and the object for auction notices. struct NotificationResponse : public MailLUBitStream { eNotificationResponse status = eNotificationResponse::UnknownError; - LWOOBJID auctionID = LWOOBJID_EMPTY; + uint64_t mailID = 0; + LWOOBJID receiverID = LWOOBJID_EMPTY; // the player's object ID + LWOOBJID attachmentID = LWOOBJID_EMPTY; + LOT attachmentLOT = LOT_NULL; + uint16_t attachmentCount = 0; uint32_t mailCount = 1; NotificationResponse() : MailLUBitStream(eMessageID::NotificationResponse) {}; void Serialize(RakNet::BitStream& bitStream) const override; @@ -210,9 +217,15 @@ namespace Mail { void HandleMail(RakNet::BitStream& inStream, const SystemAddress& sysAddr, Entity* player); + // A NewMail notice about one mail, the way live sent it: the mail, its attachment and a count of 1. + NotificationResponse NewMailNotice(const MailInfo& mail, LWOOBJID playerID); + // Part of a player's load: live told the client about unread mail right after the respawn checkpoint, without - // being asked (NotificationResponse NewMail with the count), and sent nothing when there was none. - void NotifyUnreadMailOnLoad(uint32_t unreadCount, const SystemAddress& sysAddr); + // being asked, one NewMail notice per mail, and sent nothing when there was none. + void NotifyUnreadMailOnLoad(LWOOBJID characterID, LWOOBJID playerID, const SystemAddress& sysAddr); + + // The notices NotifyUnreadMailOnLoad sends for this mail: one per unread mail, oldest first. + std::vector UnreadMailNotices(std::vector mail, LWOOBJID playerID); // 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); diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 92d5af051..ef7ac8faa 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1228,7 +1228,7 @@ void LoadPlayer(const SystemAddress& sysAddr) { invalidTransferList.bCustomerFeedbackOnInvalidMapTransfer = false; invalidTransferList.SendToClient(sysAddr); GameMessages::SendDoneLoading(player->GetObjectID(), respawnPoint, spawnPosition, spawnRotation, sysAddr); - Mail::NotifyUnreadMailOnLoad(Database::Get()->GetUnreadMailCount(c->GetID()), sysAddr); + Mail::NotifyUnreadMailOnLoad(c->GetID(), player->GetObjectID(), sysAddr); //Notify chat that a player has loaded: auto* character = player->GetCharacter(); diff --git a/tests/dGameTests/MailPacketTests.cpp b/tests/dGameTests/MailPacketTests.cpp index 199754576..100fe4e0e 100644 --- a/tests/dGameTests/MailPacketTests.cpp +++ b/tests/dGameTests/MailPacketTests.cpp @@ -129,23 +129,40 @@ TEST(MailPacketTests, NoAttachmentIsLotNull) { EXPECT_EQ(lot, -1); } -// Answer to NotificationRequest: 56 bytes, the unread count at offset 48 (live sent 2 there for two unread mails). -TEST(MailPacketTests, NotificationResponseLayout) { +// The answer to NotificationRequest names no mail: the player, no attachment (LOT -1) and the unread count at +// offset 48. Live capture (its padding held stale memory; zero here). +TEST(MailPacketTests, NotificationRequestAnswerMatchesLive) { Mail::NotificationResponse response; response.status = Mail::eNotificationResponse::NewMail; + response.receiverID = 0x100000015b7b141f; response.mailCount = 2; - const auto actual = Write(response); - ASSERT_EQ(actual.bytes.size(), 56u); - Bytes expected; - PutClientMailHeader(expected, Mail::eMessageID::NotificationResponse); - expected.Put(0); // NewMail - expected.Put(0); - expected.Put(0); - expected.Put(0); - expected.Put(0); - expected.Put(2); // mail count - expected.Put(0); - EXPECT_TRUE(PacketsEqual(expected.Packet(), actual)); + EXPECT_TRUE(PacketsEqual( + FromHex("530500310000000002000000000000000000000000000000""1f147b5b010000100000000000000000ffffffff0000000002000000""00000000"), + Write(response))); +} + +// A new mail notice: the mail, the player, the attachment and a count of 1. Live capture (padding zeroed). +TEST(MailPacketTests, NewMailNoticeMatchesLive) { + MailInfo mail; + mail.id = 0x10000001733377d7; + mail.itemID = 0x10000001733377d6; + mail.itemLOT = 0x3e6c; + mail.itemCount = 1; + EXPECT_TRUE(PacketsEqual( + FromHex("53050031000000000200000000000000d7773373010000105e7dea8e00000010d6773373010000106c3e00000100000001000000""00000000"), + Write(Mail::NewMailNotice(mail, 0x100000008eea7d5e)))); +} + +// Without an attachment the notice carries LOT -1, as live did for mission mail. +TEST(MailPacketTests, NewMailNoticeWithoutAttachment) { + MailInfo mail; + mail.id = 7; + mail.itemLOT = 0; + mail.itemCount = 1; + const auto notice = Mail::NewMailNotice(mail, 0x100000015b7b141f); + EXPECT_EQ(notice.attachmentLOT, LOT_NULL); + EXPECT_EQ(notice.mailCount, 1u); + EXPECT_EQ(Write(notice).bytes.size(), 56u); } // The mailbox opens the Mail UI with pushGameState {state: "Mail"}; bytes from a live capture. diff --git a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp index a4e6d0cd1..c830c7197 100644 --- a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp @@ -65,18 +65,34 @@ TEST_F(LoadSequenceTests, SavedCheckpointIsSentUnrotated) { EXPECT_EQ(checkpoints[0].rot, QuatUtils::IDENTITY); } -// Unread mail is announced during the load without the client asking; nothing is sent without unread mail. -// Live's packet (56 bytes) also fills the three fields DLU leaves 0 with object IDs the client does not use here. +// Unread mail is announced during the load without the client asking, one NewMail notice per unread mail (oldest +// first, a count of 1 each, as live sent two notices for two mails); nothing is sent without unread mail. TEST_F(LoadSequenceTests, UnreadMailIsAnnouncedOnLoad) { - auto sent = Capture([&] { Mail::NotifyUnreadMailOnLoad(0, ClientAddress()); }); - EXPECT_TRUE(sent.empty()); + EXPECT_TRUE(Mail::UnreadMailNotices({}, PLAYER).empty()); - sent = Capture([&] { Mail::NotifyUnreadMailOnLoad(1, ClientAddress()); }); - ASSERT_EQ(sent.size(), 1u); - EXPECT_EQ(sent[0].sysAddr, ClientAddress()); - EXPECT_PACKET_EQ(FromHex( - "53 05 00 31 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " - "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00"), FromCapture(sent[0])); + MailInfo read; + read.id = 1; + read.wasRead = true; + MailInfo newer; + newer.id = 9; + newer.itemLOT = 0; + newer.itemCount = 1; + MailInfo older; + older.id = 4; + older.itemID = 0x1000000000000123; + older.itemLOT = 3038; + older.itemCount = 100; + const auto notices = Mail::UnreadMailNotices({ read, newer, older }, PLAYER); + ASSERT_EQ(notices.size(), 2u); + EXPECT_EQ(notices[0].mailID, 4u); + EXPECT_EQ(notices[0].attachmentLOT, 3038); + EXPECT_EQ(notices[1].mailID, 9u); + EXPECT_EQ(notices[1].attachmentLOT, LOT_NULL); + for (const auto& notice : notices) { + EXPECT_EQ(notice.status, Mail::eNotificationResponse::NewMail); + EXPECT_EQ(notice.receiverID, PLAYER); + EXPECT_EQ(notice.mailCount, 1u); + } } // Live, right before TRANSFER_TO_WORLD on a rocket launch to Nimbus Station (spawn point MedPropLand): flag 32 on,