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.
This commit is contained in:
Kelsi 2026-03-27 17:20:31 -07:00
parent 6b1c728377
commit 50a3eb7f07
12 changed files with 141 additions and 28 deletions

View file

@ -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;

View file

@ -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<uint64_t>& 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<uint64_t>& itemGuids = {}) override;
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) override;

View file

@ -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<uint64_t>& itemGuids = {});
};

View file

@ -22,6 +22,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>

View file

@ -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<uint32_t>(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<char>(std::tolower(c)); });
return ext == ".blp";
};
const bool hasDir = (capeName.find('\\') != std::string::npos);
const bool hasExt = hasBlpExt(capeName);
std::vector<std::string> 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<uint16_t>(ti), capeTexture);
}
}
}
}
}
}
}
}
// --- Textures (skin atlas compositing) ---
static constexpr const char* componentDirs[] = {
"ArmUpperTexture",

View file

@ -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;

View file

@ -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<uint64_t>& itemGuids) {
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
packet.writeUInt64(mailboxGuid);

View file

@ -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<uint64_t>& 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<MailMessa
default: msg.senderEntry = packet.readUInt32(); break;
}
msg.cod = packet.readUInt32();
msg.cod = packet.readUInt64();
packet.readUInt32(); // item text id
packet.readUInt32(); // unknown
msg.stationeryId = packet.readUInt32();
msg.money = packet.readUInt32();
msg.money = packet.readUInt64();
msg.flags = packet.readUInt32();
msg.expirationTime = packet.readFloat();
msg.mailTemplateId = packet.readUInt32();

View file

@ -86,6 +86,11 @@ bool TCPSocket::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<const char*>(&one), sizeof(one));
connected = true;
LOG_INFO("Connected to ", host, ":", port);
return true;

View file

@ -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<const char*>(&one), sizeof(one));
connected = true;
LOG_INFO("Connected to world server: ", host, ":", port);
startAsyncPump();

View file

@ -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;

View file

@ -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<unsigned long long>(g),
static_cast<unsigned long long>(s),
static_cast<unsigned long long>(c));
}
// Attachments
@ -21693,9 +21705,9 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) {
ImGui::SameLine();
ImGui::Text("c");
uint32_t totalMoney = static_cast<uint32_t>(mailComposeMoney_[0]) * 10000 +
static_cast<uint32_t>(mailComposeMoney_[1]) * 100 +
static_cast<uint32_t>(mailComposeMoney_[2]);
uint64_t totalMoney = static_cast<uint64_t>(mailComposeMoney_[0]) * 10000 +
static_cast<uint64_t>(mailComposeMoney_[1]) * 100 +
static_cast<uint64_t>(mailComposeMoney_[2]);
uint32_t sendCost = attachCount > 0 ? static_cast<uint32_t>(30 * attachCount) : 30u;
ImGui::TextColored(kColorGray, "Sending cost: %uc", sendCost);