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
This commit is contained in:
Kelsi 2026-02-25 14:11:09 -08:00
parent ce30cedf4a
commit 9906269671
8 changed files with 203 additions and 17 deletions

View file

@ -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<uint64_t> 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<uint8_t>(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<int>(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<uint8_t>(19 + bagIndex);
mailAttachments_[i].srcSlot = static_cast<uint8_t>(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) {

View file

@ -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<uint64_t>& 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)

View file

@ -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<uint64_t>& 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<uint8_t>(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;

View file

@ -7384,8 +7384,8 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) {
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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<uint32_t>(mailComposeMoney_[1]) * 100 +
static_cast<uint32_t>(mailComposeMoney_[2]);
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: 30c");
uint32_t sendCost = attachCount > 0 ? static_cast<uint32_t>(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);

View file

@ -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<uint8_t>(23 + backpackIndex));
} else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
gameHandler_->depositItem(static_cast<uint8_t>(19 + bagIndex), static_cast<uint8_t>(bagSlotIndex));