From 50a3eb7f0736c54506aa25fdf4f87eda9bdcd9a4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Mar 2026 17:20:31 -0700 Subject: [PATCH] fix: mail money uint64, other-player cape textures, zone toast dedup, TCP_NODELAY Mail: change money/COD fields from uint32 to uint64 in CMSG_SEND_MAIL and SMSG_MAIL_LIST_RESULT for WotLK 3.3.5a. Classic keeps uint32 on the wire. Fixes money truncation and packet misalignment causing mail failures. Other-player capes: add cape texture loading to setOnlinePlayerEquipment(). The cape geoset was enabled but no texture was loaded, leaving capes blank. Now mirrors the local-player path: looks up ItemDisplayInfo.dbc, finds cape texture candidates, applies via setGroupTextureOverride/setTextureSlotOverride. Zone toasts: suppress duplicate zone toast when the zone text overlay is already showing the same zone name. Fixes double "Entering: Stormwind City". Network: enable TCP_NODELAY on both auth and world sockets after connect(), disabling Nagle's algorithm to eliminate up to 200ms buffering delay on small packets (movement, spell casts, chat). Rendering: track material and bone descriptor sets in M2 renderer to skip redundant vkCmdBindDescriptorSets calls between batches sharing same textures. --- include/game/game_handler.hpp | 2 +- include/game/packet_parsers.hpp | 4 +- include/game/world_packets.hpp | 6 +-- include/network/net_platform.hpp | 1 + src/core/application.cpp | 78 +++++++++++++++++++++++++++++ src/game/game_handler.cpp | 2 +- src/game/packet_parsers_classic.cpp | 2 +- src/game/world_packets.cpp | 10 ++-- src/network/tcp_socket.cpp | 5 ++ src/network/world_socket.cpp | 5 ++ src/rendering/m2_renderer.cpp | 28 ++++++++--- src/ui/game_screen.cpp | 26 +++++++--- 12 files changed, 141 insertions(+), 28 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index ef72d44e..7e51d85a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2172,7 +2172,7 @@ public: 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); + const std::string& body, uint64_t money, uint64_t cod = 0); // Mail attachments (max 12 per WotLK) static constexpr int MAIL_MAX_ATTACHMENTS = 12; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 261cae66..8ee0b255 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -261,7 +261,7 @@ 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, + uint64_t money, uint64_t cod, const std::vector& itemGuids = {}) { return SendMailPacket::build(mailboxGuid, recipient, subject, body, money, cod, itemGuids); } @@ -420,7 +420,7 @@ 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, + uint64_t money, uint64_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 itemGuidLow) override; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 2fae62e7..7c7a25f5 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2497,8 +2497,8 @@ struct MailMessage { std::string subject; std::string body; uint32_t stationeryId = 0; - uint32_t money = 0; - uint32_t cod = 0; // Cash on delivery + uint64_t money = 0; + uint64_t cod = 0; // Cash on delivery uint32_t flags = 0; float expirationTime = 0.0f; uint32_t mailTemplateId = 0; @@ -2517,7 +2517,7 @@ 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, + uint64_t money, uint64_t cod, const std::vector& itemGuids = {}); }; diff --git a/include/network/net_platform.hpp b/include/network/net_platform.hpp index 0cc38e1a..329dd375 100644 --- a/include/network/net_platform.hpp +++ b/include/network/net_platform.hpp @@ -22,6 +22,7 @@ #include #include #include + #include #include #include #include diff --git a/src/core/application.cpp b/src/core/application.cpp index 8e2130a8..00571d5e 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -7447,6 +7447,84 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, charRenderer->setActiveGeosets(st.instanceId, geosets); + // --- Cape texture (group 15 / texture type 2) --- + // The geoset above enables the cape mesh, but without a texture it renders blank. + if (hasInvType({16})) { + // Back/cloak is WoW equipment slot 14 (BACK) in the 19-element array. + uint32_t capeDid = displayInfoIds[14]; + if (capeDid != 0) { + int32_t capeRecIdx = displayInfoDbc->findRecordById(capeDid); + if (capeRecIdx >= 0) { + const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u; + std::string capeName = displayInfoDbc->getString( + static_cast(capeRecIdx), leftTexField); + + if (!capeName.empty()) { + std::replace(capeName.begin(), capeName.end(), '/', '\\'); + + auto hasBlpExt = [](const std::string& p) { + if (p.size() < 4) return false; + std::string ext = p.substr(p.size() - 4); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return ext == ".blp"; + }; + + const bool hasDir = (capeName.find('\\') != std::string::npos); + const bool hasExt = hasBlpExt(capeName); + + std::vector capeCandidates; + auto addCapeCandidate = [&](const std::string& p) { + if (p.empty()) return; + if (std::find(capeCandidates.begin(), capeCandidates.end(), p) == capeCandidates.end()) { + capeCandidates.push_back(p); + } + }; + + if (hasDir) { + addCapeCandidate(capeName); + if (!hasExt) addCapeCandidate(capeName + ".blp"); + } else { + std::string baseObj = "Item\\ObjectComponents\\Cape\\" + capeName; + std::string baseTex = "Item\\TextureComponents\\Cape\\" + capeName; + addCapeCandidate(baseObj); + addCapeCandidate(baseTex); + if (!hasExt) { + addCapeCandidate(baseObj + ".blp"); + addCapeCandidate(baseTex + ".blp"); + } + addCapeCandidate(baseObj + (st.genderId == 1 ? "_F.blp" : "_M.blp")); + addCapeCandidate(baseObj + "_U.blp"); + addCapeCandidate(baseTex + (st.genderId == 1 ? "_F.blp" : "_M.blp")); + addCapeCandidate(baseTex + "_U.blp"); + } + + const rendering::VkTexture* whiteTex = charRenderer->loadTexture(""); + rendering::VkTexture* capeTexture = nullptr; + for (const auto& candidate : capeCandidates) { + rendering::VkTexture* tex = charRenderer->loadTexture(candidate); + if (tex && tex != whiteTex) { + capeTexture = tex; + break; + } + } + + if (capeTexture) { + charRenderer->setGroupTextureOverride(st.instanceId, 15, capeTexture); + if (const auto* md = charRenderer->getModelData(st.modelId)) { + for (size_t ti = 0; ti < md->textures.size(); ti++) { + if (md->textures[ti].type == 2) { + charRenderer->setTextureSlotOverride( + st.instanceId, static_cast(ti), capeTexture); + } + } + } + } + } + } + } + } + // --- Textures (skin atlas compositing) --- static constexpr const char* componentDirs[] = { "ArmUpperTexture", diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ff0bcad7..d0a793d3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -24348,7 +24348,7 @@ void GameHandler::refreshMailList() { } void GameHandler::sendMail(const std::string& recipient, const std::string& subject, - const std::string& body, uint32_t money, uint32_t cod) { + const std::string& body, uint64_t money, uint64_t cod) { if (state != WorldState::IN_WORLD) { LOG_WARNING("sendMail: not in world"); return; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index f758f317..03e76baa 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1512,7 +1512,7 @@ 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, + uint64_t money, uint64_t cod, const std::vector& itemGuids) { network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); packet.writeUInt64(mailboxGuid); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 1b5e23a4..36d9fd17 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -5230,7 +5230,7 @@ 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, + uint64_t money, uint64_t cod, const std::vector& itemGuids) { // WotLK 3.3.5a format network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL)); @@ -5246,8 +5246,8 @@ network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& r packet.writeUInt8(i); // attachment slot index packet.writeUInt64(itemGuids[i]); } - packet.writeUInt32(money); - packet.writeUInt32(cod); + packet.writeUInt64(money); + packet.writeUInt64(cod); return packet; } @@ -5321,11 +5321,11 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector(&one), sizeof(one)); + connected = true; LOG_INFO("Connected to ", host, ":", port); return true; diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 4482e3f3..6ad5a008 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -220,6 +220,11 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) { } } + // Disable Nagle's algorithm — send small packets immediately. + int one = 1; + setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&one), sizeof(one)); + connected = true; LOG_INFO("Connected to world server: ", host, ":", port); startAsyncPump(); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 46f82382..e487f64a 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2290,6 +2290,8 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // State tracking VkPipeline currentPipeline = VK_NULL_HANDLE; + VkDescriptorSet currentMaterialSet = VK_NULL_HANDLE; + VkDescriptorSet currentBoneSet = VK_NULL_HANDLE; uint32_t frameIndex = vkCtx_->getCurrentFrame(); // Push constants struct matching m2.vert.glsl push_constant block @@ -2397,10 +2399,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const instance.bonesDirty[frameIndex] = false; } - // Bind bone descriptor set (set 2) - if (instance.boneSet[frameIndex]) { + // Bind bone descriptor set (set 2) — skip if already bound + if (instance.boneSet[frameIndex] && instance.boneSet[frameIndex] != currentBoneSet) { vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 2, 1, &instance.boneSet[frameIndex], 0, nullptr); + currentBoneSet = instance.boneSet[frameIndex]; } } @@ -2568,8 +2571,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Bind material descriptor set (set 1) — skip batch if missing // to avoid inheriting a stale descriptor set from a prior renderer if (!batch.materialSet) continue; - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr); + if (batch.materialSet != currentMaterialSet) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr); + currentMaterialSet = batch.materialSet; + } // Push constants M2PushConstants pc; @@ -2598,8 +2604,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const currentModelId = UINT32_MAX; currentModel = nullptr; currentModelValid = false; - // Reset pipeline to opaque so the first transparent bind always sets explicitly + // Reset state so the first transparent bind always sets explicitly currentPipeline = opaquePipeline_; + currentMaterialSet = VK_NULL_HANDLE; + currentBoneSet = VK_NULL_HANDLE; for (const auto& entry : sortedVisible_) { if (entry.index >= instances.size()) continue; @@ -2647,9 +2655,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const bool needsBones = modelNeedsAnimation && !instance.boneMatrices.empty(); if (needsBones && (!instance.boneBuffer[frameIndex] || !instance.boneSet[frameIndex])) continue; bool useBones = needsBones; - if (useBones && instance.boneSet[frameIndex]) { + if (useBones && instance.boneSet[frameIndex] && instance.boneSet[frameIndex] != currentBoneSet) { vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 2, 1, &instance.boneSet[frameIndex], 0, nullptr); + currentBoneSet = instance.boneSet[frameIndex]; } uint16_t desiredLOD = 0; @@ -2740,8 +2749,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const } if (!batch.materialSet) continue; - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr); + if (batch.materialSet != currentMaterialSet) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr); + currentMaterialSet = batch.materialSet; + } M2PushConstants pc; pc.model = instance.modelMatrix; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 5b97644c..cfb06a5d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -13081,6 +13081,15 @@ void GameScreen::renderZoneToasts(float deltaTime) { [](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }), zoneToasts_.end()); + // Suppress toasts while the zone text overlay is showing the same zone — + // avoids duplicate "Entering: Stormwind City" messages. + if (zoneTextTimer_ > 0.0f) { + zoneToasts_.erase( + std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), + [this](const ZoneToastEntry& e) { return e.zoneName == zoneTextName_; }), + zoneToasts_.end()); + } + if (zoneToasts_.empty()) return; auto* window = core::Application::getInstance().getWindow(); @@ -21462,11 +21471,14 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { // 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; + uint64_t g = mail.cod / 10000; + uint64_t s = (mail.cod / 100) % 100; + uint64_t c = mail.cod % 100; ImGui::TextColored(kColorRed, - "COD: %ug %us %uc (you pay this to take items)", g, s, c); + "COD: %llug %llus %lluc (you pay this to take items)", + static_cast(g), + static_cast(s), + static_cast(c)); } // Attachments @@ -21693,9 +21705,9 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { ImGui::SameLine(); ImGui::Text("c"); - uint32_t totalMoney = static_cast(mailComposeMoney_[0]) * 10000 + - static_cast(mailComposeMoney_[1]) * 100 + - static_cast(mailComposeMoney_[2]); + uint64_t totalMoney = static_cast(mailComposeMoney_[0]) * 10000 + + static_cast(mailComposeMoney_[1]) * 100 + + static_cast(mailComposeMoney_[2]); uint32_t sendCost = attachCount > 0 ? static_cast(30 * attachCount) : 30u; ImGui::TextColored(kColorGray, "Sending cost: %uc", sendCost);