feat(load): unread mail is announced during the load

Live pushed a mail NotificationResponse (NewMail, the unread count)
right after the respawn checkpoint in loads where the player had
unread mail, without the client asking (e.g. a new character's first
load: ServerDoneLoadingAllObjects, PlayerReachedRespawnCheckpoint,
NotifyMissionTask, MAIL). DLU had that call commented out, so the
mailbox icon only lit up once the client asked or new mail arrived.

It is sent only when there is unread mail (inferred: the 223 captured
loads without one are loads without unread mail; only 3 of 226 loads
carry it).

Check: send a character mail, log it out and back in without opening
the mailbox: the new-mail indicator shows right after loading; a
character with no unread mail shows none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:33:17 -05:00
parent 87ce8e3e45
commit 41ecf9d5d8
4 changed files with 28 additions and 4 deletions

View File

@@ -443,6 +443,14 @@ void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, LWOOBJ
response.Send(sysAddr);
}
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);
}
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);

View File

@@ -210,6 +210,10 @@ namespace Mail {
void HandleMail(RakNet::BitStream& inStream, const SystemAddress& sysAddr, Entity* player);
// 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);
// 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);

View File

@@ -1136,10 +1136,7 @@ void LoadPlayer(const SystemAddress& sysAddr) {
invalidTransferList.bCustomerFeedbackOnInvalidMapTransfer = false;
invalidTransferList.SendToClient(sysAddr);
GameMessages::SendDoneLoading(player->GetObjectID(), respawnPoint, spawnPosition, spawnRotation, sysAddr);
//Send the player it's mail count:
//update: this might not be needed so im going to try disabling this here.
//Mail::HandleNotificationRequest(sysAddr, player->GetObjectID());
Mail::NotifyUnreadMailOnLoad(Database::Get()->GetUnreadMailCount(c->GetID()), sysAddr);
//Notify chat that a player has loaded:
auto* character = player->GetCharacter();

View File

@@ -1,6 +1,7 @@
#include "GameDependencies.h"
#include "GameMessageTestUtils.h"
#include "Mail.h"
#include "MovementMessages.h"
#include "ZoneMessages.h"
@@ -63,3 +64,17 @@ TEST_F(LoadSequenceTests, SavedCheckpointIsSentUnrotated) {
EXPECT_EQ(checkpoints[0].pos, saved);
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.
TEST_F(LoadSequenceTests, UnreadMailIsAnnouncedOnLoad) {
auto sent = Capture([&] { Mail::NotifyUnreadMailOnLoad(0, ClientAddress()); });
EXPECT_TRUE(sent.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]));
}