Fix empty chat messages and /dismount on Classic/Turtle

Chat: renderTextWithLinks now properly handles |cAARRGGBB color codes
that aren't item links (e.g. colored player names), rendering the text
in the specified color instead of discarding it.

Dismount: Classic/Vanilla lacks CMSG_CANCEL_MOUNT_AURA (TBC+ opcode).
Track mount aura spell ID when mountDisplayId changes, then use
CMSG_CANCEL_AURA as fallback on expansions without the dedicated opcode.
This commit is contained in:
Kelsi 2026-02-14 16:42:47 -08:00
parent 8282583b9a
commit f6a0be6a08
3 changed files with 102 additions and 9 deletions

View file

@ -1401,6 +1401,7 @@ private:
TaxiOrientationCallback taxiOrientationCallback_;
TaxiFlightStartCallback taxiFlightStartCallback_;
uint32_t currentMountDisplayId_ = 0;
uint32_t mountAuraSpellId_ = 0; // Spell ID of the aura that caused mounting (for CMSG_CANCEL_AURA fallback)
float serverRunSpeed_ = 7.0f;
bool playerDead_ = false;
bool releasedSpirit_ = false;

View file

@ -3122,7 +3122,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old == 0 && val != 0) {
// Just mounted — find the mount aura (indefinite duration, self-cast)
mountAuraSpellId_ = 0;
for (const auto& a : playerAuras) {
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) {
mountAuraSpellId_ = a.spellId;
}
}
LOG_INFO("Mount detected: displayId=", val, " auraSpellId=", mountAuraSpellId_);
}
if (old != 0 && val == 0) {
mountAuraSpellId_ = 0;
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
@ -3502,7 +3513,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old == 0 && val != 0) {
mountAuraSpellId_ = 0;
for (const auto& a : playerAuras) {
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) {
mountAuraSpellId_ = a.spellId;
}
}
LOG_INFO("Mount detected (values update): displayId=", val, " auraSpellId=", mountAuraSpellId_);
}
if (old != 0 && val == 0) {
mountAuraSpellId_ = 0;
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
@ -5973,6 +5994,7 @@ void GameHandler::dismount() {
if (!socket) return;
// Clear local mount state immediately (optimistic dismount).
// Server will confirm via SMSG_UPDATE_OBJECT with mountDisplayId=0.
uint32_t savedMountAura = mountAuraSpellId_;
if (currentMountDisplayId_ != 0 || taxiMountActive_) {
if (mountCallback_) {
mountCallback_(0);
@ -5980,11 +6002,31 @@ void GameHandler::dismount() {
currentMountDisplayId_ = 0;
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
mountAuraSpellId_ = 0;
LOG_INFO("Dismount: cleared local mount state");
}
network::Packet pkt(wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA));
socket->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
// CMSG_CANCEL_MOUNT_AURA exists in TBC+ (0x0375). Classic/Vanilla doesn't have it.
uint16_t cancelMountWire = wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA);
if (cancelMountWire != 0xFFFF) {
network::Packet pkt(cancelMountWire);
socket->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
} else if (savedMountAura != 0) {
// Fallback for Classic/Vanilla: cancel the mount aura by spell ID
auto pkt = CancelAuraPacket::build(savedMountAura);
socket->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_AURA (mount spell ", savedMountAura, ") — Classic fallback");
} else {
// No tracked mount aura — try cancelling all indefinite self-cast auras
// (mount aura detection may have missed if aura arrived after mount field)
for (const auto& a : playerAuras) {
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) {
auto pkt = CancelAuraPacket::build(a.spellId);
socket->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_AURA (spell ", a.spellId, ") — brute force dismount");
}
}
}
}
void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
@ -6623,6 +6665,17 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
}
(*auraList)[slot] = aura;
}
// If player is mounted but we haven't identified the mount aura yet,
// check newly added auras (aura update may arrive after mountDisplayId)
if (data.guid == playerGuid && currentMountDisplayId_ != 0 && mountAuraSpellId_ == 0) {
for (const auto& [slot, aura] : data.updates) {
if (!aura.isEmpty() && aura.maxDurationMs < 0 && aura.casterGuid == playerGuid) {
mountAuraSpellId_ = aura.spellId;
LOG_INFO("Mount aura detected from aura update: spellId=", aura.spellId);
}
}
}
}
}

View file

@ -770,12 +770,51 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
continue;
}
// Failed to parse as item link — render the |c literally and continue
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::TextWrapped("|c");
ImGui::PopStyleColor();
ImGui::SameLine(0, 0);
pos = nextSpecial + 2;
// Not an item link — treat as colored text: |cAARRGGBB...text...|r
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
ImVec4 cColor = parseWowColor(text, linkStart);
size_t textStart = linkStart + 10; // after |cAARRGGBB
size_t resetPos2 = text.find("|r", textStart);
std::string coloredText;
if (resetPos2 != std::string::npos) {
coloredText = text.substr(textStart, resetPos2 - textStart);
pos = resetPos2 + 2; // skip |r
} else {
coloredText = text.substr(textStart);
pos = text.size();
}
// Strip any remaining WoW markup from the colored segment
// (e.g. |H...|h pairs that aren't item links)
std::string clean;
for (size_t i = 0; i < coloredText.size(); i++) {
if (coloredText[i] == '|' && i + 1 < coloredText.size()) {
char next = coloredText[i + 1];
if (next == 'H') {
// Skip |H...|h
size_t hEnd = coloredText.find("|h", i + 2);
if (hEnd != std::string::npos) { i = hEnd + 1; continue; }
} else if (next == 'h') {
i += 1; continue; // skip |h
} else if (next == 'r') {
i += 1; continue; // skip |r
}
}
clean += coloredText[i];
}
if (!clean.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, cColor);
ImGui::TextWrapped("%s", clean.c_str());
ImGui::PopStyleColor();
ImGui::SameLine(0, 0);
}
} else {
// Bare |c without enough chars for color — render literally
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::TextWrapped("|c");
ImGui::PopStyleColor();
ImGui::SameLine(0, 0);
pos = nextSpecial + 2;
}
continue;
}