From 8a468e9533aaf4febe071cb826a67e8248530f86 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 15 Feb 2026 14:00:41 -0800 Subject: [PATCH] Add mailbox system and fix logging performance stutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement full mail send/receive: SMSG_SHOW_MAILBOX, CMSG_GET_MAIL_LIST, SMSG_MAIL_LIST_RESULT, CMSG_SEND_MAIL, SMSG_SEND_MAIL_RESULT, mail take money/item/delete/mark-as-read, and inbox/compose UI windows. Fix periodic stuttering in Stormwind caused by synchronous per-line disk flushes in the logger — remove fileStream.flush() and std::endl, downgrade high-volume per-packet/per-model/per-texture LOG_INFO to LOG_DEBUG. --- Data/expansions/wotlk/opcodes.json | 13 +- include/game/game_handler.hpp | 30 +++ include/game/opcode_table.hpp | 13 ++ include/game/world_packets.hpp | 72 +++++++ include/ui/game_screen.hpp | 8 + src/core/logger.cpp | 5 +- src/game/game_handler.cpp | 288 ++++++++++++++++++++++++++- src/game/opcode_table.cpp | 24 +++ src/game/world_packets.cpp | 68 ++++++- src/network/world_socket.cpp | 6 +- src/pipeline/dbc_layout.cpp | 7 +- src/pipeline/m2_loader.cpp | 4 +- src/rendering/character_renderer.cpp | 6 +- src/ui/game_screen.cpp | 260 ++++++++++++++++++++++++ 14 files changed, 782 insertions(+), 22 deletions(-) diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json index e0c3e3e3..38a3a58b 100644 --- a/Data/expansions/wotlk/opcodes.json +++ b/Data/expansions/wotlk/opcodes.json @@ -267,5 +267,16 @@ "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", "SMSG_CHANNEL_LIST": "0x09B", - "SMSG_INSPECT_TALENT": "0x3F4" + "SMSG_INSPECT_TALENT": "0x3F4", + "SMSG_SHOW_MAILBOX": "0x24B", + "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": "0x244", + "CMSG_MAIL_DELETE": "0x243", + "CMSG_MAIL_MARK_AS_READ": "0x242", + "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 2738dab1..27862ce7 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -762,6 +762,23 @@ public: bool isVendorWindowOpen() const { return vendorWindowOpen; } const ListInventoryData& getVendorItems() const { return currentVendorItems; } + // Mail + bool isMailboxOpen() const { return mailboxOpen_; } + const std::vector& getMailInbox() const { return mailInbox_; } + 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 closeMailbox(); + void sendMail(const std::string& recipient, const std::string& subject, + const std::string& body, uint32_t money, uint32_t cod = 0); + void mailTakeMoney(uint32_t mailId); + void mailTakeItem(uint32_t mailId, uint32_t itemIndex); + void mailDelete(uint32_t mailId); + void mailMarkAsRead(uint32_t mailId); + void refreshMailList(); + // Trainer bool isTrainerWindowOpen() const { return trainerWindowOpen_; } const TrainerListData& getTrainerSpells() const { return currentTrainerList_; } @@ -987,6 +1004,12 @@ private: void handleArenaTeamEvent(network::Packet& packet); void handleArenaError(network::Packet& packet); + // ---- Mail handlers ---- + void handleShowMailbox(network::Packet& packet); + void handleMailListResult(network::Packet& packet); + void handleSendMailResult(network::Packet& packet); + void handleReceivedMail(network::Packet& packet); + // ---- Taxi handlers ---- void handleShowTaxiNodes(network::Packet& packet); void handleActivateTaxiReply(network::Packet& packet); @@ -1322,6 +1345,13 @@ private: void startClientTaxiPath(const std::vector& pathNodes); void updateClientTaxi(float deltaTime); + // Mail + bool mailboxOpen_ = false; + uint64_t mailboxGuid_ = 0; + std::vector mailInbox_; + int selectedMailIndex_ = -1; + bool showMailCompose_ = false; + // Vendor bool vendorWindowOpen = false; ListInventoryData currentVendorItems; diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index afe0d40e..f5b96796 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -365,6 +365,19 @@ enum class LogicalOpcode : uint16_t { CMSG_CHANNEL_LIST, SMSG_CHANNEL_LIST, + // ---- Mail ---- + SMSG_SHOW_MAILBOX, + CMSG_GET_MAIL_LIST, + SMSG_MAIL_LIST_RESULT, + CMSG_SEND_MAIL, + SMSG_SEND_MAIL_RESULT, + CMSG_MAIL_TAKE_MONEY, + CMSG_MAIL_TAKE_ITEM, + CMSG_MAIL_DELETE, + CMSG_MAIL_MARK_AS_READ, + SMSG_RECEIVED_MAIL, + MSG_QUERY_NEXT_MAIL_TIME, + // Sentinel COUNT }; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 9c28f43a..c92fbea7 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2166,5 +2166,77 @@ public: static network::Packet build(uint64_t casterGuid, bool accept); }; +// ============================================================ +// Mail System +// ============================================================ + +struct MailAttachment { + uint8_t slot = 0; + uint32_t itemGuidLow = 0; + uint32_t itemId = 0; + uint32_t enchantId = 0; + uint32_t randomPropertyId = 0; + uint32_t randomSuffix = 0; + uint32_t stackCount = 1; + uint32_t chargesOrDurability = 0; + uint32_t maxDurability = 0; +}; + +struct MailMessage { + uint32_t messageId = 0; + uint8_t messageType = 0; // 0=normal, 2=auction, 3=creature, 4=gameobject + uint64_t senderGuid = 0; + uint32_t senderEntry = 0; // For non-player mail + std::string senderName; + std::string subject; + std::string body; + uint32_t stationeryId = 0; + uint32_t money = 0; + uint32_t cod = 0; // Cash on delivery + uint32_t flags = 0; + float expirationTime = 0.0f; + uint32_t mailTemplateId = 0; + bool read = false; + std::vector attachments; +}; + +/** CMSG_GET_MAIL_LIST packet builder */ +class GetMailListPacket { +public: + static network::Packet build(uint64_t mailboxGuid); +}; + +/** CMSG_SEND_MAIL packet builder */ +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); +}; + +/** CMSG_MAIL_TAKE_MONEY packet builder */ +class MailTakeMoneyPacket { +public: + static network::Packet build(uint64_t mailboxGuid, uint32_t mailId); +}; + +/** CMSG_MAIL_TAKE_ITEM packet builder */ +class MailTakeItemPacket { +public: + static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex); +}; + +/** CMSG_MAIL_DELETE packet builder */ +class MailDeletePacket { +public: + static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId); +}; + +/** CMSG_MAIL_MARK_AS_READ packet builder */ +class MailMarkAsReadPacket { +public: + static network::Packet build(uint64_t mailboxGuid, uint32_t mailId); +}; + } // namespace game } // namespace wowee diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 415f8f6d..abf53255 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -181,6 +181,8 @@ private: void renderGuildRoster(game::GameHandler& gameHandler); void renderGuildInvitePopup(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler); + void renderMailWindow(game::GameHandler& gameHandler); + void renderMailComposeWindow(game::GameHandler& gameHandler); /** * Inventory screen @@ -242,6 +244,12 @@ private: std::vector chatBubbles_; bool chatBubbleCallbackSet_ = false; + // Mail compose state + char mailRecipientBuffer_[256] = ""; + char mailSubjectBuffer_[256] = ""; + char mailBodyBuffer_[2048] = ""; + int mailComposeMoney_[3] = {0, 0, 0}; // gold, silver, copper + // Left-click targeting: distinguish click from camera drag glm::vec2 leftClickPressPos_ = glm::vec2(0.0f); bool leftClickWasPress_ = false; diff --git a/src/core/logger.cpp b/src/core/logger.cpp index 3038282a..6073f797 100644 --- a/src/core/logger.cpp +++ b/src/core/logger.cpp @@ -54,10 +54,9 @@ void Logger::log(LogLevel level, const std::string& message) { line << "] " << message; - std::cout << line.str() << std::endl; + std::cout << line.str() << '\n'; if (fileStream.is_open()) { - fileStream << line.str() << std::endl; - fileStream.flush(); + fileStream << line.str() << '\n'; } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8f7e06db..1584b949 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1508,6 +1508,20 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + // ---- Mail ---- + case Opcode::SMSG_SHOW_MAILBOX: + handleShowMailbox(packet); + break; + case Opcode::SMSG_MAIL_LIST_RESULT: + handleMailListResult(packet); + break; + case Opcode::SMSG_SEND_MAIL_RESULT: + handleSendMailResult(packet); + break; + case Opcode::SMSG_RECEIVED_MAIL: + handleReceivedMail(packet); + break; + default: // In pre-world states we need full visibility (char create/login handshakes). // In-world we keep de-duplication to avoid heavy log I/O in busy areas. @@ -3978,7 +3992,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) { } void GameHandler::handleDestroyObject(network::Packet& packet) { - LOG_INFO("Handling SMSG_DESTROY_OBJECT"); + LOG_DEBUG("Handling SMSG_DESTROY_OBJECT"); DestroyObjectData data; if (!DestroyObjectParser::parse(packet, data)) { @@ -9605,6 +9619,278 @@ void GameHandler::updateAttachedTransportChildren(float /*deltaTime*/) { } } +// ============================================================ +// Mail System +// ============================================================ + +void GameHandler::closeMailbox() { + mailboxOpen_ = false; + mailboxGuid_ = 0; + mailInbox_.clear(); + selectedMailIndex_ = -1; + showMailCompose_ = false; +} + +void GameHandler::refreshMailList() { + if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; + auto packet = GetMailListPacket::build(mailboxGuid_); + socket->send(packet); +} + +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); + socket->send(packet); +} + +void GameHandler::mailTakeMoney(uint32_t mailId) { + if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; + auto packet = MailTakeMoneyPacket::build(mailboxGuid_, mailId); + socket->send(packet); +} + +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); + socket->send(packet); +} + +void GameHandler::mailDelete(uint32_t mailId) { + if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; + // Find mail template ID for this mail + uint32_t templateId = 0; + for (const auto& m : mailInbox_) { + if (m.messageId == mailId) { + templateId = m.mailTemplateId; + break; + } + } + auto packet = MailDeletePacket::build(mailboxGuid_, mailId, templateId); + socket->send(packet); +} + +void GameHandler::mailMarkAsRead(uint32_t mailId) { + if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; + auto packet = MailMarkAsReadPacket::build(mailboxGuid_, mailId); + socket->send(packet); +} + +void GameHandler::handleShowMailbox(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 8) { + LOG_WARNING("SMSG_SHOW_MAILBOX too short"); + return; + } + uint64_t guid = packet.readUInt64(); + LOG_INFO("SMSG_SHOW_MAILBOX: guid=0x", std::hex, guid, std::dec); + mailboxGuid_ = guid; + mailboxOpen_ = true; + selectedMailIndex_ = -1; + showMailCompose_ = false; + // Request inbox contents + refreshMailList(); +} + +void GameHandler::handleMailListResult(network::Packet& packet) { + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 5) { + LOG_WARNING("SMSG_MAIL_LIST_RESULT too short (", remaining, " bytes)"); + return; + } + + uint32_t totalCount = packet.readUInt32(); + uint8_t shownCount = packet.readUInt8(); + + 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 + if (msg.messageType == 0 && msg.senderGuid != 0) { + msg.senderName = getCachedPlayerName(msg.senderGuid); + if (msg.senderName.empty()) { + queryPlayerName(msg.senderGuid); + msg.senderName = "Unknown"; + } + } else if (msg.messageType == 2) { + msg.senderName = "Auction House"; + } else if (msg.messageType == 3) { + msg.senderName = getCachedCreatureName(msg.senderEntry); + if (msg.senderName.empty()) msg.senderName = "NPC"; + } 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"); +} + +void GameHandler::handleSendMailResult(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 12) { + LOG_WARNING("SMSG_SEND_MAIL_RESULT too short"); + return; + } + + uint32_t mailId = packet.readUInt32(); + uint32_t command = packet.readUInt32(); + 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, ... + static const char* cmdNames[] = {"Send", "TakeMoney", "TakeItem", "Return", "Delete", "MadePermanent"}; + const char* cmdName = (command < 6) ? cmdNames[command] : "Unknown"; + + LOG_INFO("SMSG_SEND_MAIL_RESULT: mailId=", mailId, " cmd=", cmdName, " error=", error); + + if (error == 0) { + // Success + switch (command) { + case 0: // Send + addSystemChatMessage("Mail sent successfully."); + showMailCompose_ = false; + refreshMailList(); + break; + case 1: // Money taken + addSystemChatMessage("Money received from mail."); + refreshMailList(); + break; + case 2: // Item taken + addSystemChatMessage("Item received from mail."); + refreshMailList(); + break; + case 4: // Deleted + selectedMailIndex_ = -1; + refreshMailList(); + break; + default: + refreshMailList(); + break; + } + } else { + // Error + 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; + default: errMsg += "Unknown error (" + std::to_string(error) + ")."; break; + } + addSystemChatMessage(errMsg); + } +} + +void GameHandler::handleReceivedMail(network::Packet& packet) { + // Server notifies us that new mail arrived + if (packet.getSize() - packet.getReadPos() >= 4) { + float nextMailTime = packet.readFloat(); + (void)nextMailTime; + } + LOG_INFO("SMSG_RECEIVED_MAIL: New mail arrived!"); + addSystemChatMessage("New mail has arrived."); + // If mailbox is open, refresh + if (mailboxOpen_) { + refreshMailList(); + } +} + 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 c0705e00..34bf10ce 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -291,6 +291,18 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"CMSG_CHANNEL_LIST", LogicalOpcode::CMSG_CHANNEL_LIST}, {"SMSG_CHANNEL_LIST", LogicalOpcode::SMSG_CHANNEL_LIST}, {"SMSG_INSPECT_TALENT", LogicalOpcode::SMSG_INSPECT_TALENT}, + // Mail + {"SMSG_SHOW_MAILBOX", LogicalOpcode::SMSG_SHOW_MAILBOX}, + {"CMSG_GET_MAIL_LIST", LogicalOpcode::CMSG_GET_MAIL_LIST}, + {"SMSG_MAIL_LIST_RESULT", LogicalOpcode::SMSG_MAIL_LIST_RESULT}, + {"CMSG_SEND_MAIL", LogicalOpcode::CMSG_SEND_MAIL}, + {"SMSG_SEND_MAIL_RESULT", LogicalOpcode::SMSG_SEND_MAIL_RESULT}, + {"CMSG_MAIL_TAKE_MONEY", LogicalOpcode::CMSG_MAIL_TAKE_MONEY}, + {"CMSG_MAIL_TAKE_ITEM", LogicalOpcode::CMSG_MAIL_TAKE_ITEM}, + {"CMSG_MAIL_DELETE", LogicalOpcode::CMSG_MAIL_DELETE}, + {"CMSG_MAIL_MARK_AS_READ", LogicalOpcode::CMSG_MAIL_MARK_AS_READ}, + {"SMSG_RECEIVED_MAIL", LogicalOpcode::SMSG_RECEIVED_MAIL}, + {"MSG_QUERY_NEXT_MAIL_TIME", LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME}, }; // clang-format on @@ -583,6 +595,18 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::CMSG_CHANNEL_LIST, 0x09A}, {LogicalOpcode::SMSG_CHANNEL_LIST, 0x09B}, {LogicalOpcode::SMSG_INSPECT_TALENT, 0x3F4}, + // Mail + {LogicalOpcode::SMSG_SHOW_MAILBOX, 0x24B}, + {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::SMSG_RECEIVED_MAIL, 0x285}, + {LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME, 0x284}, }; logicalToWire_.clear(); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 91588488..29351afc 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1116,9 +1116,9 @@ bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data data.isDeath = false; } - LOG_INFO("Parsed SMSG_DESTROY_OBJECT:"); - LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec); - LOG_INFO(" Is death: ", data.isDeath ? "yes" : "no"); + LOG_DEBUG("Parsed SMSG_DESTROY_OBJECT:"); + LOG_DEBUG(" GUID: 0x", std::hex, data.guid, std::dec); + LOG_DEBUG(" Is death: ", data.isDeath ? "yes" : "no"); return true; } @@ -2101,7 +2101,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa data.armor = static_cast(packet.readUInt32()); data.valid = !data.name.empty(); - LOG_INFO("Item query response: ", data.name, " (quality=", data.quality, + LOG_DEBUG("Item query response: ", data.name, " (quality=", data.quality, " invType=", data.inventoryType, " stack=", data.maxStack, ")"); return true; } @@ -2463,7 +2463,7 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } } - LOG_INFO("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); + LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); return true; } @@ -2485,7 +2485,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.missCount = packet.readUInt8(); // Skip miss details for now - LOG_INFO("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, + LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); return true; } @@ -3303,5 +3303,61 @@ network::Packet GameObjectUsePacket::build(uint64_t guid) { return packet; } +// ============================================================ +// Mail System +// ============================================================ + +network::Packet GetMailListPacket::build(uint64_t mailboxGuid) { + network::Packet packet(wireOpcode(Opcode::CMSG_GET_MAIL_LIST)); + packet.writeUInt64(mailboxGuid); + return packet; +} + +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) { + 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); // unknown + packet.writeUInt8(0); // attachment count (no item attachments for now) + packet.writeUInt32(money); + packet.writeUInt32(cod); + return packet; +} + +network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_MONEY)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + return packet; +} + +network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + packet.writeUInt32(itemIndex); + return packet; +} + +network::Packet MailDeletePacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + packet.writeUInt32(mailTemplateId); + return packet; +} + +network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailId) { + network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_MARK_AS_READ)); + packet.writeUInt64(mailboxGuid); + packet.writeUInt32(mailId); + return packet; +} + } // namespace game } // namespace wowee diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index c36e83b1..996d6f07 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -296,7 +296,7 @@ void WorldSocket::update() { } if (receivedAny) { - LOG_INFO("World socket read ", bytesReadThisTick, " bytes in ", readOps, + LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps, " recv call(s), buffered=", receiveBuffer.size()); // Hex dump received bytes for auth debugging if (bytesReadThisTick <= 128) { @@ -304,11 +304,11 @@ void WorldSocket::update() { for (size_t i = 0; i < receiveBuffer.size(); ++i) { char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf; } - LOG_INFO("World socket raw bytes: ", hex); + LOG_DEBUG("World socket raw bytes: ", hex); } tryParsePackets(); if (connected && !receiveBuffer.empty()) { - LOG_INFO("World socket parse left ", receiveBuffer.size(), + LOG_DEBUG("World socket parse left ", receiveBuffer.size(), " bytes buffered (awaiting complete packet)"); } } diff --git a/src/pipeline/dbc_layout.cpp b/src/pipeline/dbc_layout.cpp index 8b9e5669..7dc1f1af 100644 --- a/src/pipeline/dbc_layout.cpp +++ b/src/pipeline/dbc_layout.cpp @@ -23,10 +23,11 @@ void DBCLayout::loadWotlkDefaults() { { "InventoryIcon", 5 }, { "GeosetGroup1", 7 }, { "GeosetGroup3", 9 }}}; // CharSections.dbc - // Binary layout: ID(0) Race(1) Sex(2) Section(3) Tex1(4) Tex2(5) Tex3(6) Flags(7) Variation(8) Color(9) + // Binary layout: ID(0) Race(1) Sex(2) Section(3) Variation(4) Color(5) Tex1(6) Tex2(7) Tex3(8) Flags(9) layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 }, - { "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 }, - { "Flags", 7 }, { "VariationIndex", 8 }, { "ColorIndex", 9 }}}; + { "VariationIndex", 4 }, { "ColorIndex", 5 }, + { "Texture1", 6 }, { "Texture2", 7 }, { "Texture3", 8 }, + { "Flags", 9 }}}; // SpellIcon.dbc (Icon.dbc in code but actually SpellIcon) layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}}; diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index 0590ee44..8bba4688 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -744,7 +744,7 @@ M2Model M2Loader::load(const std::vector& m2Data) { header.nParticleEmitters = r32(); header.ofsParticleEmitters = r32(); - core::Logger::getInstance().info("Vanilla M2 (version ", header.version, + core::Logger::getInstance().debug("Vanilla M2 (version ", header.version, "): nVerts=", header.nVertices, " nViews=", header.nViews, " ofsViews=", ofsViews, " nTex=", header.nTextures); } else { @@ -1315,7 +1315,7 @@ M2Model M2Loader::load(const std::vector& m2Data) { } } - core::Logger::getInstance().info("Vanilla M2: embedded skin loaded — ", + core::Logger::getInstance().debug("Vanilla M2: embedded skin loaded — ", model.indices.size(), " indices, ", model.batches.size(), " batches"); } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 7d2695a2..205a665a 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -657,7 +657,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, composite[dstIdx + 3] = base.data[srcIdx + 3]; } } - core::Logger::getInstance().info("compositeWithRegions: upscaled 256x256 to 512x512"); + core::Logger::getInstance().debug("compositeWithRegions: upscaled 256x256 to 512x512"); } else { composite = base.data; } @@ -748,7 +748,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, blitOverlay(composite, width, height, overlay, dstX, dstY); } - core::Logger::getInstance().info("compositeWithRegions: region ", regionIdx, + core::Logger::getInstance().debug("compositeWithRegions: region ", regionIdx, " at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second); } @@ -765,7 +765,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, applyAnisotropicFiltering(); glBindTexture(GL_TEXTURE_2D, 0); - core::Logger::getInstance().info("compositeWithRegions: created ", width, "x", height, + core::Logger::getInstance().debug("compositeWithRegions: created ", width, "x", height, " texture with ", regionLayers.size(), " equipment regions"); compositeCache_[cacheKey] = texId; return texId; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index b096d624..02066b3d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -252,6 +252,8 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); renderTaxiWindow(gameHandler); + renderMailWindow(gameHandler); + renderMailComposeWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); @@ -6028,4 +6030,262 @@ void GameScreen::loadSettings() { LOG_INFO("Settings loaded from ", path); } +// ============================================================ +// Mail Window +// ============================================================ + +void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isMailboxOpen()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Appearing); + + bool open = true; + if (ImGui::Begin("Mailbox", &open)) { + const auto& inbox = gameHandler.getMailInbox(); + + // Top bar: money + compose button + uint64_t money = gameHandler.getMoneyCopper(); + uint32_t mg = static_cast(money / 10000); + uint32_t ms = static_cast((money / 100) % 100); + uint32_t mc = static_cast(money % 100); + ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); + ImGui::SameLine(ImGui::GetWindowWidth() - 100); + if (ImGui::Button("Compose")) { + mailRecipientBuffer_[0] = '\0'; + mailSubjectBuffer_[0] = '\0'; + mailBodyBuffer_[0] = '\0'; + mailComposeMoney_[0] = 0; + mailComposeMoney_[1] = 0; + mailComposeMoney_[2] = 0; + gameHandler.openMailCompose(); + } + ImGui::Separator(); + + if (inbox.empty()) { + ImGui::TextDisabled("No mail."); + } else { + // Two-panel layout: left = mail list, right = selected mail detail + float listWidth = 220.0f; + + // Left panel - mail list + ImGui::BeginChild("MailList", ImVec2(listWidth, 0), true); + for (size_t i = 0; i < inbox.size(); ++i) { + const auto& mail = inbox[i]; + ImGui::PushID(static_cast(i)); + + bool selected = (gameHandler.getSelectedMailIndex() == static_cast(i)); + std::string label = mail.subject.empty() ? "(No Subject)" : mail.subject; + + // Unread indicator + if (!mail.read) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.5f, 1.0f)); + } + + if (ImGui::Selectable(label.c_str(), selected)) { + gameHandler.setSelectedMailIndex(static_cast(i)); + // Mark as read + if (!mail.read) { + gameHandler.mailMarkAsRead(mail.messageId); + } + } + + if (!mail.read) { + ImGui::PopStyleColor(); + } + + // Sub-info line + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), " From: %s", mail.senderName.c_str()); + if (mail.money > 0) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), " [G]"); + } + if (!mail.attachments.empty()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]"); + } + + ImGui::PopID(); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + // Right panel - selected mail detail + ImGui::BeginChild("MailDetail", ImVec2(0, 0), true); + int sel = gameHandler.getSelectedMailIndex(); + if (sel >= 0 && sel < static_cast(inbox.size())) { + const auto& mail = inbox[sel]; + + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s", + mail.subject.empty() ? "(No Subject)" : mail.subject.c_str()); + ImGui::Text("From: %s", mail.senderName.c_str()); + + if (mail.messageType == 2) { + ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]"); + } + ImGui::Separator(); + + // Body text + if (!mail.body.empty()) { + ImGui::TextWrapped("%s", mail.body.c_str()); + ImGui::Separator(); + } + + // Money + if (mail.money > 0) { + uint32_t g = mail.money / 10000; + uint32_t s = (mail.money / 100) % 100; + uint32_t c = mail.money % 100; + ImGui::Text("Money: %ug %us %uc", g, s, c); + ImGui::SameLine(); + if (ImGui::SmallButton("Take Money")) { + gameHandler.mailTakeMoney(mail.messageId); + } + } + + // COD warning + if (mail.cod > 0) { + uint32_t g = mail.cod / 10000; + uint32_t s = (mail.cod / 100) % 100; + uint32_t c = mail.cod % 100; + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + "COD: %ug %us %uc (you pay this to take items)", g, s, c); + } + + // Attachments + if (!mail.attachments.empty()) { + ImGui::Text("Attachments: %zu", mail.attachments.size()); + for (size_t j = 0; j < mail.attachments.size(); ++j) { + const auto& att = mail.attachments[j]; + ImGui::PushID(static_cast(j)); + + auto* info = gameHandler.getItemInfo(att.itemId); + if (info && info->valid) { + ImGui::BulletText("%s x%u", info->name.c_str(), att.stackCount); + } else { + ImGui::BulletText("Item %u x%u", att.itemId, att.stackCount); + gameHandler.ensureItemInfo(att.itemId); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Take")) { + gameHandler.mailTakeItem(mail.messageId, att.slot); + } + + ImGui::PopID(); + } + } + + ImGui::Spacing(); + ImGui::Separator(); + + // Action buttons + if (ImGui::Button("Delete")) { + gameHandler.mailDelete(mail.messageId); + } + ImGui::SameLine(); + if (mail.messageType == 0 && ImGui::Button("Reply")) { + // Pre-fill compose with sender as recipient + strncpy(mailRecipientBuffer_, mail.senderName.c_str(), sizeof(mailRecipientBuffer_) - 1); + mailRecipientBuffer_[sizeof(mailRecipientBuffer_) - 1] = '\0'; + std::string reSubject = "Re: " + mail.subject; + strncpy(mailSubjectBuffer_, reSubject.c_str(), sizeof(mailSubjectBuffer_) - 1); + mailSubjectBuffer_[sizeof(mailSubjectBuffer_) - 1] = '\0'; + mailBodyBuffer_[0] = '\0'; + mailComposeMoney_[0] = 0; + mailComposeMoney_[1] = 0; + mailComposeMoney_[2] = 0; + gameHandler.openMailCompose(); + } + } else { + ImGui::TextDisabled("Select a mail to read."); + } + ImGui::EndChild(); + } + } + ImGui::End(); + + if (!open) { + gameHandler.closeMailbox(); + } +} + +void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isMailComposeOpen()) return; + + auto* window = core::Application::getInstance().getWindow(); + 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); + + bool open = true; + if (ImGui::Begin("Send Mail", &open)) { + ImGui::Text("To:"); + ImGui::SameLine(60); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##MailTo", mailRecipientBuffer_, sizeof(mailRecipientBuffer_)); + + ImGui::Text("Subject:"); + ImGui::SameLine(60); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##MailSubject", mailSubjectBuffer_, sizeof(mailSubjectBuffer_)); + + ImGui::Text("Body:"); + ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_), + ImVec2(-1, 150)); + + ImGui::Text("Money:"); + ImGui::SameLine(60); + ImGui::SetNextItemWidth(60); + ImGui::InputInt("##MailGold", &mailComposeMoney_[0], 0, 0); + if (mailComposeMoney_[0] < 0) mailComposeMoney_[0] = 0; + ImGui::SameLine(); + ImGui::Text("g"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(40); + ImGui::InputInt("##MailSilver", &mailComposeMoney_[1], 0, 0); + if (mailComposeMoney_[1] < 0) mailComposeMoney_[1] = 0; + if (mailComposeMoney_[1] > 99) mailComposeMoney_[1] = 99; + ImGui::SameLine(); + ImGui::Text("s"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(40); + ImGui::InputInt("##MailCopper", &mailComposeMoney_[2], 0, 0); + if (mailComposeMoney_[2] < 0) mailComposeMoney_[2] = 0; + if (mailComposeMoney_[2] > 99) mailComposeMoney_[2] = 99; + ImGui::SameLine(); + ImGui::Text("c"); + + uint32_t totalMoney = static_cast(mailComposeMoney_[0]) * 10000 + + static_cast(mailComposeMoney_[1]) * 100 + + static_cast(mailComposeMoney_[2]); + + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: 30c"); + + ImGui::Spacing(); + bool canSend = (strlen(mailRecipientBuffer_) > 0); + if (!canSend) ImGui::BeginDisabled(); + if (ImGui::Button("Send", ImVec2(80, 0))) { + gameHandler.sendMail(mailRecipientBuffer_, mailSubjectBuffer_, + mailBodyBuffer_, totalMoney); + } + if (!canSend) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + gameHandler.closeMailCompose(); + } + } + ImGui::End(); + + if (!open) { + gameHandler.closeMailCompose(); + } +} + }} // namespace wowee::ui