From 9906269671c3ee63c8ed369fd8957172926aced5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 25 Feb 2026 14:11:09 -0800 Subject: [PATCH] Add mail item attachment support for sending - CMSG_SEND_MAIL now includes item GUIDs (up to 12 per WotLK) - Right-click items in bags to attach when mail compose is open - Compose window shows 12-slot attachment grid with item icons - Click attached items to remove them - Classic/Vanilla falls back to single item GUID format --- include/game/game_handler.hpp | 21 +++++- include/game/packet_parsers.hpp | 8 ++- include/game/world_packets.hpp | 3 +- src/game/game_handler.cpp | 108 +++++++++++++++++++++++++++- src/game/packet_parsers_classic.cpp | 7 +- src/game/world_packets.cpp | 10 ++- src/ui/game_screen.cpp | 57 +++++++++++++-- src/ui/inventory_screen.cpp | 6 +- 8 files changed, 203 insertions(+), 17 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d0364020..34cca73d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -856,12 +856,28 @@ public: int getSelectedMailIndex() const { return selectedMailIndex_; } void setSelectedMailIndex(int idx) { selectedMailIndex_ = idx; } bool isMailComposeOpen() const { return showMailCompose_; } - void openMailCompose() { showMailCompose_ = true; } - void closeMailCompose() { showMailCompose_ = false; } + void openMailCompose() { showMailCompose_ = true; clearMailAttachments(); } + void closeMailCompose() { showMailCompose_ = false; clearMailAttachments(); } 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); + + // Mail attachments (max 12 per WotLK) + static constexpr int MAIL_MAX_ATTACHMENTS = 12; + struct MailAttachSlot { + uint64_t itemGuid = 0; + game::ItemDef item; + uint8_t srcBag = 0xFF; // source container for return + uint8_t srcSlot = 0; + bool occupied() const { return itemGuid != 0; } + }; + bool attachItemFromBackpack(int backpackIndex); + bool attachItemFromBag(int bagIndex, int slotIndex); + bool detachMailAttachment(int attachIndex); + void clearMailAttachments(); + const std::array& getMailAttachments() const { return mailAttachments_; } + int getMailAttachmentCount() const; void mailTakeMoney(uint32_t mailId); void mailTakeItem(uint32_t mailId, uint32_t itemIndex); void mailDelete(uint32_t mailId); @@ -1568,6 +1584,7 @@ private: int selectedMailIndex_ = -1; bool showMailCompose_ = false; bool hasNewMail_ = false; + std::array mailAttachments_{}; // Bank bool bankOpen_ = false; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index edd97e8c..15a271bd 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -217,8 +217,9 @@ public: /** 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); + uint32_t money, uint32_t cod, + const std::vector& itemGuids = {}) { + return SendMailPacket::build(mailboxGuid, recipient, subject, body, money, cod, itemGuids); } /** Parse SMSG_MAIL_LIST_RESULT into a vector of MailMessage */ @@ -323,7 +324,8 @@ public: 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; + uint32_t money, uint32_t cod, + const std::vector& itemGuids = {}) 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/include/game/world_packets.hpp b/include/game/world_packets.hpp index 9124d75e..95e17f6d 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2290,7 +2290,8 @@ class SendMailPacket { public: static network::Packet build(uint64_t mailboxGuid, const std::string& recipient, const std::string& subject, const std::string& body, - uint32_t money, uint32_t cod); + uint32_t money, uint32_t cod, + const std::vector& itemGuids = {}); }; /** CMSG_MAIL_TAKE_MONEY packet builder */ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4f1c844a..3a6aea5c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12851,10 +12851,114 @@ void GameHandler::sendMail(const std::string& recipient, const std::string& subj LOG_WARNING("sendMail: mailboxGuid_ is 0 (mailbox closed?)"); return; } - auto packet = packetParsers_->buildSendMail(mailboxGuid_, recipient, subject, body, money, cod); + // Collect attached item GUIDs + std::vector itemGuids; + for (const auto& att : mailAttachments_) { + if (att.occupied()) { + itemGuids.push_back(att.itemGuid); + } + } + auto packet = packetParsers_->buildSendMail(mailboxGuid_, recipient, subject, body, money, cod, itemGuids); LOG_INFO("sendMail: to='", recipient, "' subject='", subject, "' money=", money, - " mailboxGuid=", mailboxGuid_); + " attachments=", itemGuids.size(), " mailboxGuid=", mailboxGuid_); socket->send(packet); + clearMailAttachments(); +} + +bool GameHandler::attachItemFromBackpack(int backpackIndex) { + if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return false; + const auto& slot = inventory.getBackpackSlot(backpackIndex); + if (slot.empty()) return false; + + uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; + if (itemGuid == 0) { + itemGuid = resolveOnlineItemGuid(slot.item.itemId); + } + if (itemGuid == 0) { + addSystemChatMessage("Cannot attach: item not found."); + return false; + } + + // Check not already attached + for (const auto& att : mailAttachments_) { + if (att.occupied() && att.itemGuid == itemGuid) return false; + } + + // Find free attachment slot + for (int i = 0; i < MAIL_MAX_ATTACHMENTS; ++i) { + if (!mailAttachments_[i].occupied()) { + mailAttachments_[i].itemGuid = itemGuid; + mailAttachments_[i].item = slot.item; + mailAttachments_[i].srcBag = 0xFF; + mailAttachments_[i].srcSlot = static_cast(23 + backpackIndex); + LOG_INFO("Mail attach: slot=", i, " item='", slot.item.name, "' guid=0x", + std::hex, itemGuid, std::dec, " from backpack[", backpackIndex, "]"); + return true; + } + } + addSystemChatMessage("Cannot attach: all attachment slots full."); + return false; +} + +bool GameHandler::attachItemFromBag(int bagIndex, int slotIndex) { + if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return false; + if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return false; + const auto& slot = inventory.getBagSlot(bagIndex, slotIndex); + if (slot.empty()) return false; + + uint64_t itemGuid = 0; + uint64_t bagGuid = equipSlotGuids_[19 + bagIndex]; + if (bagGuid != 0) { + auto it = containerContents_.find(bagGuid); + if (it != containerContents_.end() && slotIndex < static_cast(it->second.numSlots)) { + itemGuid = it->second.slotGuids[slotIndex]; + } + } + if (itemGuid == 0) { + itemGuid = resolveOnlineItemGuid(slot.item.itemId); + } + if (itemGuid == 0) { + addSystemChatMessage("Cannot attach: item not found."); + return false; + } + + for (const auto& att : mailAttachments_) { + if (att.occupied() && att.itemGuid == itemGuid) return false; + } + + for (int i = 0; i < MAIL_MAX_ATTACHMENTS; ++i) { + if (!mailAttachments_[i].occupied()) { + mailAttachments_[i].itemGuid = itemGuid; + mailAttachments_[i].item = slot.item; + mailAttachments_[i].srcBag = static_cast(19 + bagIndex); + mailAttachments_[i].srcSlot = static_cast(slotIndex); + LOG_INFO("Mail attach: slot=", i, " item='", slot.item.name, "' guid=0x", + std::hex, itemGuid, std::dec, " from bag[", bagIndex, "][", slotIndex, "]"); + return true; + } + } + addSystemChatMessage("Cannot attach: all attachment slots full."); + return false; +} + +bool GameHandler::detachMailAttachment(int attachIndex) { + if (attachIndex < 0 || attachIndex >= MAIL_MAX_ATTACHMENTS) return false; + if (!mailAttachments_[attachIndex].occupied()) return false; + LOG_INFO("Mail detach: slot=", attachIndex, " item='", mailAttachments_[attachIndex].item.name, "'"); + mailAttachments_[attachIndex] = MailAttachSlot{}; + return true; +} + +void GameHandler::clearMailAttachments() { + for (auto& att : mailAttachments_) att = MailAttachSlot{}; +} + +int GameHandler::getMailAttachmentCount() const { + int count = 0; + for (const auto& att : mailAttachments_) { + if (att.occupied()) ++count; + } + return count; } void GameHandler::mailTakeMoney(uint32_t mailId) { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 6dcfe934..a6e81764 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -728,7 +728,8 @@ 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) { + uint32_t money, uint32_t cod, + const std::vector& itemGuids) { network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); packet.writeUInt64(mailboxGuid); packet.writeString(recipient); @@ -736,7 +737,9 @@ network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid, packet.writeString(body); packet.writeUInt32(0); // stationery packet.writeUInt32(0); // unknown - packet.writeUInt64(0); // item GUID (0 = no attachment, single item only in Vanilla) + // Vanilla supports only one item attachment (single uint64 GUID) + uint64_t singleItemGuid = itemGuids.empty() ? 0 : itemGuids[0]; + packet.writeUInt64(singleItemGuid); packet.writeUInt32(money); packet.writeUInt32(cod); packet.writeUInt64(0); // unk3 (clients > 1.9.4) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 51b7bf44..b181284e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3939,7 +3939,8 @@ 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) { + uint32_t money, uint32_t cod, + const std::vector& itemGuids) { // WotLK 3.3.5a format network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); packet.writeUInt64(mailboxGuid); @@ -3948,7 +3949,12 @@ network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& r packet.writeString(body); packet.writeUInt32(0); // stationery packet.writeUInt32(0); // unknown - packet.writeUInt8(0); // attachment count (0 = no attachments) + uint8_t attachCount = static_cast(itemGuids.size()); + packet.writeUInt8(attachCount); + for (uint8_t i = 0; i < attachCount; ++i) { + packet.writeUInt8(i); // attachment slot index + packet.writeUInt64(itemGuids[i]); + } packet.writeUInt32(money); packet.writeUInt32(cod); return packet; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 618af2c9..2f7bfda9 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -7384,8 +7384,8 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; - ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, screenH / 2 - 200), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(380, 400), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 190, screenH / 2 - 250), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Send Mail", &open)) { @@ -7401,8 +7401,56 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { ImGui::Text("Body:"); ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_), - ImVec2(-1, 150)); + ImVec2(-1, 120)); + // Attachments section + int attachCount = gameHandler.getMailAttachmentCount(); + ImGui::Text("Attachments (%d/12):", attachCount); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Right-click items in bags to attach"); + + const auto& attachments = gameHandler.getMailAttachments(); + // Show attachment slots in a grid (6 per row) + for (int i = 0; i < game::GameHandler::MAIL_MAX_ATTACHMENTS; ++i) { + if (i % 6 != 0) ImGui::SameLine(); + ImGui::PushID(i + 5000); + const auto& att = attachments[i]; + if (att.occupied()) { + // Show item with quality color border + ImVec4 qualColor = ui::InventoryScreen::getQualityColor(att.item.quality); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qualColor.x * 0.3f, qualColor.y * 0.3f, qualColor.z * 0.3f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qualColor.x * 0.5f, qualColor.y * 0.5f, qualColor.z * 0.5f, 0.9f)); + + // Try to show icon + VkDescriptorSet icon = inventoryScreen.getItemIcon(att.item.displayInfoId); + bool clicked = false; + if (icon) { + clicked = ImGui::ImageButton("##att", (ImTextureID)icon, ImVec2(30, 30)); + } else { + // Truncate name to fit + std::string label = att.item.name.substr(0, 4); + clicked = ImGui::Button(label.c_str(), ImVec2(36, 36)); + } + ImGui::PopStyleColor(2); + + if (clicked) { + gameHandler.detachMailAttachment(i); + } + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextColored(qualColor, "%s", att.item.name.c_str()); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Click to remove"); + ImGui::EndTooltip(); + } + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.5f)); + ImGui::Button("##empty", ImVec2(36, 36)); + ImGui::PopStyleColor(); + } + ImGui::PopID(); + } + + ImGui::Spacing(); ImGui::Text("Money:"); ImGui::SameLine(60); ImGui::SetNextItemWidth(60); @@ -7429,7 +7477,8 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { static_cast(mailComposeMoney_[1]) * 100 + static_cast(mailComposeMoney_[2]); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: 30c"); + uint32_t sendCost = attachCount > 0 ? static_cast(30 * attachCount) : 30u; + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: %uc", sendCost); ImGui::Spacing(); bool canSend = (strlen(mailRecipientBuffer_) > 0); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 757df530..3d7f03dc 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1428,7 +1428,11 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite " bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex, " vendorMode=", vendorMode_, " bankOpen=", gameHandler_->isBankOpen()); - if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) { + if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) { + gameHandler_->attachItemFromBackpack(backpackIndex); + } else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) { + gameHandler_->attachItemFromBag(bagIndex, bagSlotIndex); + } else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) { gameHandler_->depositItem(0xFF, static_cast(23 + backpackIndex)); } else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && isBagSlot) { gameHandler_->depositItem(static_cast(19 + bagIndex), static_cast(bagSlotIndex));