diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index d2ab7d63..338b9684 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -238,5 +238,16 @@ "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", "SMSG_CHANNEL_LIST": "0x09B", - "SMSG_INSPECT_TALENT": "0x3F4" + "SMSG_INSPECT_TALENT": "0x3F4", + "SMSG_SHOW_MAILBOX": "0x297", + "CMSG_GET_MAIL_LIST": "0x23A", + "SMSG_MAIL_LIST_RESULT": "0x23B", + "CMSG_SEND_MAIL": "0x238", + "SMSG_SEND_MAIL_RESULT": "0x239", + "CMSG_MAIL_TAKE_MONEY": "0x245", + "CMSG_MAIL_TAKE_ITEM": "0x246", + "CMSG_MAIL_DELETE": "0x249", + "CMSG_MAIL_MARK_AS_READ": "0x247", + "SMSG_RECEIVED_MAIL": "0x285", + "MSG_QUERY_NEXT_MAIL_TIME": "0x284" } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 27862ce7..d458ed7f 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -770,6 +770,7 @@ public: bool isMailComposeOpen() const { return showMailCompose_; } void openMailCompose() { showMailCompose_ = true; } void closeMailCompose() { showMailCompose_ = false; } + bool hasNewMail() const { return hasNewMail_; } void closeMailbox(); void sendMail(const std::string& recipient, const std::string& subject, const std::string& body, uint32_t money, uint32_t cod = 0); @@ -1009,6 +1010,7 @@ private: void handleMailListResult(network::Packet& packet); void handleSendMailResult(network::Packet& packet); void handleReceivedMail(network::Packet& packet); + void handleQueryNextMailTime(network::Packet& packet); // ---- Taxi handlers ---- void handleShowTaxiNodes(network::Packet& packet); @@ -1351,6 +1353,7 @@ private: std::vector mailInbox_; int selectedMailIndex_ = -1; bool showMailCompose_ = false; + bool hasNewMail_ = false; // Vendor bool vendorWindowOpen = false; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 28b8c4b8..34ed09c5 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -167,6 +167,28 @@ public: return LeaveChannelPacket::build(channelName); } + // --- Mail --- + + /** Build CMSG_SEND_MAIL */ + virtual network::Packet buildSendMail(uint64_t mailboxGuid, const std::string& recipient, + const std::string& subject, const std::string& body, + uint32_t money, uint32_t cod) { + return SendMailPacket::build(mailboxGuid, recipient, subject, body, money, cod); + } + + /** Parse SMSG_MAIL_LIST_RESULT into a vector of MailMessage */ + virtual bool parseMailList(network::Packet& packet, std::vector& inbox); + + /** Build CMSG_MAIL_TAKE_ITEM */ + virtual network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) { + return MailTakeItemPacket::build(mailboxGuid, mailId, itemSlot); + } + + /** Build CMSG_MAIL_DELETE */ + virtual network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) { + return MailDeletePacket::build(mailboxGuid, mailId, mailTemplateId); + } + // --- Utility --- /** Read a packed GUID from the packet */ @@ -252,6 +274,12 @@ public: bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override; network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) override; network::Packet buildLeaveChannel(const std::string& channelName) override; + network::Packet buildSendMail(uint64_t mailboxGuid, const std::string& recipient, + const std::string& subject, const std::string& body, + uint32_t money, uint32_t cod) override; + bool parseMailList(network::Packet& packet, std::vector& inbox) override; + network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) override; + network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override; }; /** diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b865e83d..199e59e1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1521,6 +1521,9 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_RECEIVED_MAIL: handleReceivedMail(packet); break; + case Opcode::MSG_QUERY_NEXT_MAIL_TIME: + handleQueryNextMailTime(packet); + break; default: // In pre-world states we need full visibility (char create/login handshakes). @@ -5401,6 +5404,13 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) { msg.senderName = data.name; } } + + // Backfill mail inbox sender names + for (auto& mail : mailInbox_) { + if (mail.messageType == 0 && mail.senderGuid == data.guid) { + mail.senderName = data.name; + } + } } } @@ -7343,8 +7353,39 @@ void GameHandler::interactWithNpc(uint64_t guid) { void GameHandler::interactWithGameObject(uint64_t guid) { if (state != WorldState::IN_WORLD || !socket) return; + + // Rate-limit to prevent spamming the server + static uint64_t lastInteractGuid = 0; + static std::chrono::steady_clock::time_point lastInteractTime{}; + auto now = std::chrono::steady_clock::now(); + if (guid == lastInteractGuid && + std::chrono::duration_cast(now - lastInteractTime).count() < 1000) { + return; // Ignore repeated clicks within 1 second + } + lastInteractGuid = guid; + lastInteractTime = now; + + auto entity = entityManager.getEntity(guid); + auto packet = GameObjectUsePacket::build(guid); socket->send(packet); + + // For mailbox GameObjects (type 19), open mail UI and request mail list. + // In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends + // animation/sound and expects the client to request the mail list. + if (entity && entity->getType() == ObjectType::GAMEOBJECT) { + auto go = std::static_pointer_cast(entity); + auto* info = getCachedGameObjectInfo(go->getEntry()); + if (info && info->type == 19) { + LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list"); + mailboxGuid_ = guid; + mailboxOpen_ = true; + hasNewMail_ = false; + selectedMailIndex_ = -1; + showMailCompose_ = false; + refreshMailList(); + } + } } void GameHandler::selectGossipOption(uint32_t optionId) { @@ -9639,8 +9680,21 @@ void GameHandler::refreshMailList() { void GameHandler::sendMail(const std::string& recipient, const std::string& subject, const std::string& body, uint32_t money, uint32_t cod) { - if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; - auto packet = SendMailPacket::build(mailboxGuid_, recipient, subject, body, money, cod); + if (state != WorldState::IN_WORLD) { + LOG_WARNING("sendMail: not in world"); + return; + } + if (!socket) { + LOG_WARNING("sendMail: no socket"); + return; + } + if (mailboxGuid_ == 0) { + LOG_WARNING("sendMail: mailboxGuid_ is 0 (mailbox closed?)"); + return; + } + auto packet = packetParsers_->buildSendMail(mailboxGuid_, recipient, subject, body, money, cod); + LOG_INFO("sendMail: to='", recipient, "' subject='", subject, "' money=", money, + " mailboxGuid=", mailboxGuid_); socket->send(packet); } @@ -9652,7 +9706,7 @@ void GameHandler::mailTakeMoney(uint32_t mailId) { void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemIndex) { if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; - auto packet = MailTakeItemPacket::build(mailboxGuid_, mailId, itemIndex); + auto packet = packetParsers_->buildMailTakeItem(mailboxGuid_, mailId, itemIndex); socket->send(packet); } @@ -9666,7 +9720,7 @@ void GameHandler::mailDelete(uint32_t mailId) { break; } } - auto packet = MailDeletePacket::build(mailboxGuid_, mailId, templateId); + auto packet = packetParsers_->buildMailDelete(mailboxGuid_, mailId, templateId); socket->send(packet); } @@ -9685,6 +9739,7 @@ void GameHandler::handleShowMailbox(network::Packet& packet) { LOG_INFO("SMSG_SHOW_MAILBOX: guid=0x", std::hex, guid, std::dec); mailboxGuid_ = guid; mailboxOpen_ = true; + hasNewMail_ = false; selectedMailIndex_ = -1; showMailCompose_ = false; // Request inbox contents @@ -9693,95 +9748,16 @@ void GameHandler::handleShowMailbox(network::Packet& packet) { void GameHandler::handleMailListResult(network::Packet& packet) { size_t remaining = packet.getSize() - packet.getReadPos(); - if (remaining < 5) { + if (remaining < 1) { LOG_WARNING("SMSG_MAIL_LIST_RESULT too short (", remaining, " bytes)"); return; } - uint32_t totalCount = packet.readUInt32(); - uint8_t shownCount = packet.readUInt8(); + // Delegate parsing to expansion-aware packet parser + packetParsers_->parseMailList(packet, mailInbox_); - LOG_INFO("SMSG_MAIL_LIST_RESULT: total=", totalCount, " shown=", (int)shownCount); - - mailInbox_.clear(); - mailInbox_.reserve(shownCount); - - for (uint8_t i = 0; i < shownCount; ++i) { - remaining = packet.getSize() - packet.getReadPos(); - if (remaining < 2) break; - - // Read size of this mail entry (uint16) - uint16_t msgSize = packet.readUInt16(); - size_t startPos = packet.getReadPos(); - - MailMessage msg; - if (remaining < static_cast(msgSize) + 2) { - LOG_WARNING("Mail entry ", i, " truncated"); - break; - } - - msg.messageId = packet.readUInt32(); - msg.messageType = packet.readUInt8(); - - switch (msg.messageType) { - case 0: // Normal player mail - msg.senderGuid = packet.readUInt64(); - break; - case 2: // Auction - case 3: // Creature - case 4: // GameObject - case 5: // Calendar - msg.senderEntry = packet.readUInt32(); - break; - default: - msg.senderEntry = packet.readUInt32(); - break; - } - - msg.cod = packet.readUInt32(); - packet.readUInt32(); // unknown / item text id - packet.readUInt32(); // unknown - msg.stationeryId = packet.readUInt32(); - msg.money = packet.readUInt32(); - msg.flags = packet.readUInt32(); - msg.expirationTime = packet.readFloat(); - msg.mailTemplateId = packet.readUInt32(); - msg.subject = packet.readString(); - - // Body - only present if not a mail template - if (msg.mailTemplateId == 0) { - msg.body = packet.readString(); - } - - // Attachments - uint8_t attachCount = packet.readUInt8(); - msg.attachments.reserve(attachCount); - for (uint8_t j = 0; j < attachCount; ++j) { - MailAttachment att; - att.slot = packet.readUInt8(); - att.itemGuidLow = packet.readUInt32(); - att.itemId = packet.readUInt32(); - - // Enchantments (7 slots: id, duration, charges per slot = 21 uint32s) - for (int e = 0; e < 7; ++e) { - uint32_t enchId = packet.readUInt32(); - packet.readUInt32(); // duration - packet.readUInt32(); // charges - if (e == 0) att.enchantId = enchId; - } - - att.randomPropertyId = packet.readUInt32(); - att.randomSuffix = packet.readUInt32(); - att.stackCount = packet.readUInt32(); - att.chargesOrDurability = packet.readUInt32(); - att.maxDurability = packet.readUInt32(); - - msg.attachments.push_back(att); - } - - msg.read = (msg.flags & 0x01) != 0; // MAIL_CHECK_MASK_READ - - // Resolve sender name for player mail + // Resolve sender names (needs GameHandler context, so done here) + for (auto& msg : mailInbox_) { if (msg.messageType == 0 && msg.senderGuid != 0) { msg.senderName = getCachedPlayerName(msg.senderGuid); if (msg.senderName.empty()) { @@ -9796,20 +9772,16 @@ void GameHandler::handleMailListResult(network::Packet& packet) { } else { msg.senderName = "System"; } - - mailInbox_.push_back(std::move(msg)); - - // Skip any unread bytes in this mail entry - size_t consumed = packet.getReadPos() - startPos; - if (consumed < msgSize) { - size_t skip = msgSize - consumed; - for (size_t s = 0; s < skip && packet.getReadPos() < packet.getSize(); ++s) { - packet.readUInt8(); - } - } } - LOG_INFO("Parsed ", mailInbox_.size(), " mail messages"); + // Open the mailbox UI if it isn't already open (Vanilla has no SMSG_SHOW_MAILBOX). + if (!mailboxOpen_) { + LOG_INFO("Opening mailbox UI (triggered by SMSG_MAIL_LIST_RESULT)"); + mailboxOpen_ = true; + hasNewMail_ = false; + selectedMailIndex_ = -1; + showMailCompose_ = false; + } } void GameHandler::handleSendMailResult(network::Packet& packet) { @@ -9823,7 +9795,7 @@ void GameHandler::handleSendMailResult(network::Packet& packet) { uint32_t error = packet.readUInt32(); // Commands: 0=send, 1=moneyTaken, 2=itemTaken, 3=returnedToSender, 4=deleted, 5=madePermanent - // Errors: 0=OK, 1=equip, 2=cannotSend, 3=messageTooBig, 4=noMoney, ... + // Vanilla errors: 0=OK, 1=equipError, 2=cannotSendToSelf, 3=notEnoughMoney, 4=recipientNotFound, 5=notYourTeam, 6=internalError static const char* cmdNames[] = {"Send", "TakeMoney", "TakeItem", "Return", "Delete", "MadePermanent"}; const char* cmdName = (command < 6) ? cmdNames[command] : "Unknown"; @@ -9858,19 +9830,17 @@ void GameHandler::handleSendMailResult(network::Packet& packet) { std::string errMsg = "Mail error: "; switch (error) { case 1: errMsg += "Equipment error."; break; - case 2: errMsg += "Cannot send mail."; break; - case 3: errMsg += "Message too big."; break; - case 4: errMsg += "Not enough money."; break; - case 5: errMsg += "Not enough items."; break; - case 6: errMsg += "Recipient not found."; break; - case 7: errMsg += "Cannot send to that player."; break; - case 8: errMsg += "Equip error."; break; - case 9: errMsg += "Inventory full."; break; - case 10: errMsg += "Not a GM."; break; - case 11: errMsg += "Max attachments exceeded."; break; - case 14: errMsg += "Cannot send wrapped COD."; break; - case 15: errMsg += "Mail and chat suspended."; break; - case 16: errMsg += "Too many attachments."; break; + case 2: errMsg += "You cannot send mail to yourself."; break; + case 3: errMsg += "Not enough money."; break; + case 4: errMsg += "Recipient not found."; break; + case 5: errMsg += "Cannot send to the opposing faction."; break; + case 6: errMsg += "Internal mail error."; break; + case 14: errMsg += "Disabled for trial accounts."; break; + case 15: errMsg += "Recipient's mailbox is full."; break; + case 16: errMsg += "Cannot send wrapped items COD."; break; + case 17: errMsg += "Mail and chat suspended."; break; + case 18: errMsg += "Too many attachments."; break; + case 19: errMsg += "Invalid attachment."; break; default: errMsg += "Unknown error (" + std::to_string(error) + ")."; break; } addSystemChatMessage(errMsg); @@ -9884,6 +9854,7 @@ void GameHandler::handleReceivedMail(network::Packet& packet) { (void)nextMailTime; } LOG_INFO("SMSG_RECEIVED_MAIL: New mail arrived!"); + hasNewMail_ = true; addSystemChatMessage("New mail has arrived."); // If mailbox is open, refresh if (mailboxOpen_) { @@ -9891,6 +9862,25 @@ void GameHandler::handleReceivedMail(network::Packet& packet) { } } +void GameHandler::handleQueryNextMailTime(network::Packet& packet) { + // Server response to MSG_QUERY_NEXT_MAIL_TIME + // If there's pending mail, the packet contains a float with time until next mail delivery + // A value of 0.0 or the presence of mail entries means there IS mail waiting + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining >= 4) { + float nextMailTime = packet.readFloat(); + // In Vanilla: 0x00000000 = has mail, 0xC7A8C000 (big negative) = no mail + uint32_t rawValue; + std::memcpy(&rawValue, &nextMailTime, sizeof(uint32_t)); + if (rawValue == 0 || nextMailTime >= 0.0f) { + hasNewMail_ = true; + LOG_INFO("MSG_QUERY_NEXT_MAIL_TIME: Player has pending mail"); + } else { + LOG_INFO("MSG_QUERY_NEXT_MAIL_TIME: No pending mail (value=", nextMailTime, ")"); + } + } +} + glm::vec3 GameHandler::getComposedWorldPosition() { if (playerTransportGuid_ != 0 && transportManager_) { return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 34bf10ce..ad90ea12 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -488,7 +488,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_GOSSIP_MESSAGE, 0x17D}, {LogicalOpcode::SMSG_GOSSIP_COMPLETE, 0x17E}, {LogicalOpcode::SMSG_NPC_TEXT_UPDATE, 0x180}, - {LogicalOpcode::CMSG_GAMEOBJECT_USE, 0x01B}, + {LogicalOpcode::CMSG_GAMEOBJECT_USE, 0x0B1}, {LogicalOpcode::CMSG_QUESTGIVER_STATUS_QUERY, 0x182}, {LogicalOpcode::SMSG_QUESTGIVER_STATUS, 0x183}, {LogicalOpcode::SMSG_QUESTGIVER_STATUS_MULTIPLE, 0x198}, @@ -596,15 +596,15 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_CHANNEL_LIST, 0x09B}, {LogicalOpcode::SMSG_INSPECT_TALENT, 0x3F4}, // Mail - {LogicalOpcode::SMSG_SHOW_MAILBOX, 0x24B}, + {LogicalOpcode::SMSG_SHOW_MAILBOX, 0x297}, {LogicalOpcode::CMSG_GET_MAIL_LIST, 0x23A}, {LogicalOpcode::SMSG_MAIL_LIST_RESULT, 0x23B}, {LogicalOpcode::CMSG_SEND_MAIL, 0x238}, {LogicalOpcode::SMSG_SEND_MAIL_RESULT, 0x239}, {LogicalOpcode::CMSG_MAIL_TAKE_MONEY, 0x245}, - {LogicalOpcode::CMSG_MAIL_TAKE_ITEM, 0x244}, - {LogicalOpcode::CMSG_MAIL_DELETE, 0x243}, - {LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x242}, + {LogicalOpcode::CMSG_MAIL_TAKE_ITEM, 0x246}, + {LogicalOpcode::CMSG_MAIL_DELETE, 0x249}, + {LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x247}, {LogicalOpcode::SMSG_RECEIVED_MAIL, 0x285}, {LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME, 0x284}, }; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index e9069917..fbd9ba48 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -698,5 +698,147 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes return true; } +// ============================================================================ +// Classic CMSG_SEND_MAIL — Vanilla 1.12 format +// Differences from WotLK: +// - Single uint64 itemGuid instead of uint8 attachmentCount + item array +// - Trailing uint64 unk3 + uint8 unk4 (clients > 1.9.4) +// ============================================================================ +network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid, + const std::string& recipient, + const std::string& subject, + const std::string& body, + uint32_t money, uint32_t cod) { + network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); + packet.writeUInt64(mailboxGuid); + packet.writeString(recipient); + packet.writeString(subject); + packet.writeString(body); + packet.writeUInt32(0); // stationery + packet.writeUInt32(0); // unknown + packet.writeUInt64(0); // item GUID (0 = no attachment, single item only in Vanilla) + packet.writeUInt32(money); + packet.writeUInt32(cod); + packet.writeUInt64(0); // unk3 (clients > 1.9.4) + packet.writeUInt8(0); // unk4 (clients > 1.9.4) + return packet; +} + +// ============================================================================ +// Classic SMSG_MAIL_LIST_RESULT — Vanilla 1.12 format (per vmangos) +// Key differences from WotLK: +// - uint8 count (not uint32 totalCount + uint8 shownCount) +// - No msgSize prefix per entry +// - Subject comes before item data +// - Single inline item (not attachment count + array) +// - uint8 stackCount (not uint32) +// - No enchantment array (single permanentEnchant uint32) +// ============================================================================ +bool ClassicPacketParsers::parseMailList(network::Packet& packet, + std::vector& inbox) { + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 1) return false; + + uint8_t count = packet.readUInt8(); + LOG_INFO("SMSG_MAIL_LIST_RESULT (Classic): count=", (int)count); + + inbox.clear(); + inbox.reserve(count); + + for (uint8_t i = 0; i < count; ++i) { + remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 5) { + LOG_WARNING("Classic mail entry ", i, " truncated (", remaining, " bytes left)"); + break; + } + + MailMessage msg; + + // vmangos HandleGetMailList format: + // u32 messageId, u8 messageType, sender (guid or u32), + // string subject, u32 itemTextId, u32 package, u32 stationery, + // item fields (entry, enchant, randomProp, suffixFactor, + // u8 stackCount, u32 charges, u32 maxDur, u32 dur), + // u32 money, u32 cod, u32 flags, float expirationTime, + // u32 mailTemplateId (build-dependent) + msg.messageId = packet.readUInt32(); + msg.messageType = packet.readUInt8(); + + switch (msg.messageType) { + case 0: msg.senderGuid = packet.readUInt64(); break; + default: msg.senderEntry = packet.readUInt32(); break; + } + + msg.subject = packet.readString(); + + uint32_t itemTextId = packet.readUInt32(); + (void)itemTextId; + packet.readUInt32(); // package (unused) + msg.stationeryId = packet.readUInt32(); + + // Single inline item (Vanilla: one item per mail) + uint32_t itemEntry = packet.readUInt32(); + uint32_t permanentEnchant = packet.readUInt32(); + uint32_t randomPropertyId = packet.readUInt32(); + uint32_t suffixFactor = packet.readUInt32(); + uint8_t stackCount = packet.readUInt8(); + packet.readUInt32(); // charges + uint32_t maxDurability = packet.readUInt32(); + uint32_t durability = packet.readUInt32(); + + if (itemEntry != 0) { + MailAttachment att; + att.slot = 0; + att.itemGuidLow = 0; // Not provided in Vanilla list + att.itemId = itemEntry; + att.enchantId = permanentEnchant; + att.randomPropertyId = randomPropertyId; + att.randomSuffix = suffixFactor; + att.stackCount = stackCount; + att.chargesOrDurability = durability; + att.maxDurability = maxDurability; + msg.attachments.push_back(att); + } + + msg.money = packet.readUInt32(); + msg.cod = packet.readUInt32(); + msg.flags = packet.readUInt32(); + msg.expirationTime = packet.readFloat(); + msg.mailTemplateId = packet.readUInt32(); + + msg.read = (msg.flags & 0x01) != 0; + inbox.push_back(std::move(msg)); + } + + LOG_INFO("Parsed ", inbox.size(), " mail messages"); + return true; +} + +// ============================================================================ +// Classic CMSG_MAIL_TAKE_ITEM — Vanilla only sends mailboxGuid + mailId +// (no itemSlot — Vanilla only supports 1 item per mail) +// ============================================================================ +network::Packet ClassicPacketParsers::buildMailTakeItem(uint64_t mailboxGuid, + uint32_t mailId, + uint32_t /*itemSlot*/) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + return packet; +} + +// ============================================================================ +// Classic CMSG_MAIL_DELETE — Vanilla only sends mailboxGuid + mailId +// (no mailTemplateId field) +// ============================================================================ +network::Packet ClassicPacketParsers::buildMailDelete(uint64_t mailboxGuid, + uint32_t mailId, + uint32_t /*mailTemplateId*/) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + return packet; +} + } // namespace game } // namespace wowee diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 29351afc..9905980e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1,4 +1,5 @@ #include "game/world_packets.hpp" +#include "game/packet_parsers.hpp" #include "game/opcodes.hpp" #include "game/character.hpp" #include "auth/crypto.hpp" @@ -3316,14 +3317,15 @@ network::Packet GetMailListPacket::build(uint64_t mailboxGuid) { network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient, const std::string& subject, const std::string& body, uint32_t money, uint32_t cod) { + // WotLK 3.3.5a format network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); packet.writeUInt64(mailboxGuid); packet.writeString(recipient); packet.writeString(subject); packet.writeString(body); - packet.writeUInt32(0); // stationery (default) + packet.writeUInt32(0); // stationery packet.writeUInt32(0); // unknown - packet.writeUInt8(0); // attachment count (no item attachments for now) + packet.writeUInt8(0); // attachment count (0 = no attachments) packet.writeUInt32(money); packet.writeUInt32(cod); return packet; @@ -3359,5 +3361,95 @@ network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailI return packet; } +// ============================================================================ +// PacketParsers::parseMailList — WotLK 3.3.5a format (base/default) +// ============================================================================ +bool PacketParsers::parseMailList(network::Packet& packet, std::vector& inbox) { + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 5) return false; + + uint32_t totalCount = packet.readUInt32(); + uint8_t shownCount = packet.readUInt8(); + (void)totalCount; + + LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", (int)shownCount); + + inbox.clear(); + inbox.reserve(shownCount); + + for (uint8_t i = 0; i < shownCount; ++i) { + remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 2) break; + + uint16_t msgSize = packet.readUInt16(); + size_t startPos = packet.getReadPos(); + + MailMessage msg; + if (remaining < static_cast(msgSize) + 2) { + LOG_WARNING("Mail entry ", i, " truncated"); + break; + } + + msg.messageId = packet.readUInt32(); + msg.messageType = packet.readUInt8(); + + switch (msg.messageType) { + case 0: msg.senderGuid = packet.readUInt64(); break; + case 2: case 3: case 4: case 5: + msg.senderEntry = packet.readUInt32(); break; + default: msg.senderEntry = packet.readUInt32(); break; + } + + msg.cod = packet.readUInt32(); + packet.readUInt32(); // item text id + packet.readUInt32(); // unknown + msg.stationeryId = packet.readUInt32(); + msg.money = packet.readUInt32(); + msg.flags = packet.readUInt32(); + msg.expirationTime = packet.readFloat(); + msg.mailTemplateId = packet.readUInt32(); + msg.subject = packet.readString(); + + if (msg.mailTemplateId == 0) { + msg.body = packet.readString(); + } + + uint8_t attachCount = packet.readUInt8(); + msg.attachments.reserve(attachCount); + for (uint8_t j = 0; j < attachCount; ++j) { + MailAttachment att; + att.slot = packet.readUInt8(); + att.itemGuidLow = packet.readUInt32(); + att.itemId = packet.readUInt32(); + for (int e = 0; e < 7; ++e) { + uint32_t enchId = packet.readUInt32(); + packet.readUInt32(); // duration + packet.readUInt32(); // charges + if (e == 0) att.enchantId = enchId; + } + att.randomPropertyId = packet.readUInt32(); + att.randomSuffix = packet.readUInt32(); + att.stackCount = packet.readUInt32(); + att.chargesOrDurability = packet.readUInt32(); + att.maxDurability = packet.readUInt32(); + msg.attachments.push_back(att); + } + + msg.read = (msg.flags & 0x01) != 0; + inbox.push_back(std::move(msg)); + + // Skip unread bytes + size_t consumed = packet.getReadPos() - startPos; + if (consumed < msgSize) { + size_t skip = msgSize - consumed; + for (size_t s = 0; s < skip && packet.getReadPos() < packet.getSize(); ++s) + packet.readUInt8(); + } + } + + LOG_INFO("Parsed ", inbox.size(), " mail messages"); + return true; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 415b8f8e..7f04c816 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1199,8 +1199,14 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { heightOffset = 0.3f; } } else if (t == game::ObjectType::GAMEOBJECT) { - hitRadius = 1.2f; - heightOffset = 0.8f; + // Check GO type — skip non-interactable decorations + auto go = std::static_pointer_cast(entity); + auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry()); + uint32_t goType = goInfo ? goInfo->type : 0; + // Type 5 = GENERIC (decorations), skip + if (goType == 5) continue; + hitRadius = 2.5f; + heightOffset = 1.2f; } hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); hitCenter.z += heightOffset; @@ -1262,8 +1268,14 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { heightOffset = 0.3f; } } else if (t == game::ObjectType::GAMEOBJECT) { - hitRadius = 1.2f; - heightOffset = 0.8f; + // Check GO type — skip non-interactable decorations + auto go = std::static_pointer_cast(entity); + auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry()); + uint32_t goType = goInfo ? goInfo->type : 0; + // Type 5 = GENERIC (decorations), skip + if (goType == 5) continue; + hitRadius = 2.5f; + heightOffset = 1.2f; } hitCenter = core::coords::canonicalToRender( glm::vec3(entity->getX(), entity->getY(), entity->getZ())); @@ -5728,6 +5740,23 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } ImGui::End(); + + // "New Mail" indicator below the minimap + if (gameHandler.hasNewMail()) { + float indicatorX = centerX - mapRadius; + float indicatorY = centerY + mapRadius + 4.0f; + ImGui::SetNextWindowPos(ImVec2(indicatorX, indicatorY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(mapRadius * 2.0f, 22), ImGuiCond_Always); + ImGuiWindowFlags mailFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs; + if (ImGui::Begin("##NewMailIndicator", nullptr, mailFlags)) { + // Pulsing effect + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 3.0f); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, pulse), "New Mail!"); + } + ImGui::End(); + } } std::string GameScreen::getSettingsPath() {