From 91dc45d19efebd215f6bff212330010f6a5182d2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 20:23:24 -0700 Subject: [PATCH] fix(combatlog): dedupe duplicate spellsteal aura logs --- include/game/game_handler.hpp | 9 ++++ src/game/game_handler.cpp | 98 ++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 1f04eb22..7ce07326 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2369,6 +2369,7 @@ private: void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0, uint64_t srcGuid = 0, uint64_t dstGuid = 0); + bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId); void addSystemChatMessage(const std::string& message); /** @@ -2575,7 +2576,15 @@ private: std::unordered_set hostileAttackers_; std::vector combatText; static constexpr size_t MAX_COMBAT_LOG = 500; + struct RecentSpellstealLogEntry { + uint64_t casterGuid = 0; + uint64_t victimGuid = 0; + uint32_t spellId = 0; + std::chrono::steady_clock::time_point timestamp{}; + }; + static constexpr size_t MAX_RECENT_SPELLSTEAL_LOGS = 32; std::deque combatLog_; + std::deque recentSpellstealLogs_; std::deque areaTriggerMsgs_; // unitGuid → sorted threat list (descending by threat value) std::unordered_map> threatLists_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 03600d42..1a7bf71d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6164,43 +6164,56 @@ void GameHandler::handlePacket(network::Packet& packet) { // multi-aura packets down to the first entry only. std::vector dispelledIds; dispelledIds.reserve(count); - std::string firstSpellName; for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 5; ++i) { uint32_t dispelledId = packet.readUInt32(); /*uint8_t isPositive =*/ packet.readUInt8(); if (dispelledId != 0) { dispelledIds.push_back(dispelledId); } - if (i == 0) { - const std::string& nm = getSpellName(dispelledId); - firstSpellName = nm.empty() ? ("spell " + std::to_string(dispelledId)) : nm; - } } // Show system message if player was victim or caster if (victimGuid == playerGuid || casterGuid == playerGuid) { - if (!firstSpellName.empty()) { + std::vector loggedIds; + if (isStolen) { + loggedIds.reserve(dispelledIds.size()); + for (uint32_t dispelledId : dispelledIds) { + if (shouldLogSpellstealAura(casterGuid, victimGuid, dispelledId)) + loggedIds.push_back(dispelledId); + } + } else { + loggedIds = dispelledIds; + } + + const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0; + const std::string displaySpellName = displaySpellId != 0 + ? [&]() { + const std::string& nm = getSpellName(displaySpellId); + return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm; + }() + : std::string{}; + if (!displaySpellName.empty()) { char buf[256]; if (isStolen) { if (victimGuid == playerGuid && casterGuid != playerGuid) - std::snprintf(buf, sizeof(buf), "%s was stolen.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); else if (casterGuid == playerGuid) - std::snprintf(buf, sizeof(buf), "You steal %s.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellName.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was stolen.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); } else { if (victimGuid == playerGuid && casterGuid != playerGuid) - std::snprintf(buf, sizeof(buf), "%s was dispelled.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str()); else if (casterGuid == playerGuid) - std::snprintf(buf, sizeof(buf), "You dispel %s.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellName.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was dispelled.", firstSpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str()); } addSystemChatMessage(buf); } // Preserve stolen auras as spellsteal events so the log wording stays accurate. - if (!dispelledIds.empty()) { + if (!loggedIds.empty()) { bool isPlayerCaster = (casterGuid == playerGuid); - for (uint32_t dispelledId : dispelledIds) { + for (uint32_t dispelledId : loggedIds) { addCombatText(isStolen ? CombatTextEntry::STEAL : CombatTextEntry::DISPEL, 0, dispelledId, isPlayerCaster, 0, casterGuid, victimGuid); @@ -6236,31 +6249,41 @@ void GameHandler::handlePacket(network::Packet& packet) { // Preserve every stolen aura in the combat log instead of only the first. std::vector stolenIds; stolenIds.reserve(stealCount); - std::string stolenName; for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= 5; ++i) { uint32_t stolenId = packet.readUInt32(); /*uint8_t isPos =*/ packet.readUInt8(); if (stolenId != 0) { stolenIds.push_back(stolenId); } - if (i == 0) { - const std::string& nm = getSpellName(stolenId); - stolenName = nm.empty() ? ("spell " + std::to_string(stolenId)) : nm; - } } if (stealCaster == playerGuid || stealVictim == playerGuid) { - if (!stolenName.empty()) { + std::vector loggedIds; + loggedIds.reserve(stolenIds.size()); + for (uint32_t stolenId : stolenIds) { + if (shouldLogSpellstealAura(stealCaster, stealVictim, stolenId)) + loggedIds.push_back(stolenId); + } + + const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0; + const std::string displaySpellName = displaySpellId != 0 + ? [&]() { + const std::string& nm = getSpellName(displaySpellId); + return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm; + }() + : std::string{}; + if (!displaySpellName.empty()) { char buf[256]; if (stealCaster == playerGuid) - std::snprintf(buf, sizeof(buf), "You stole %s.", stolenName.c_str()); + std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellName.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was stolen.", stolenName.c_str()); + std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); addSystemChatMessage(buf); } - // Preserve spellsteal as a distinct event so the UI wording stays accurate. - if (!stolenIds.empty()) { + // Some servers emit both SPELLDISPELLOG(isStolen=1) and SPELLSTEALLOG + // for the same aura. Keep the first event and suppress the duplicate. + if (!loggedIds.empty()) { bool isPlayerCaster = (stealCaster == playerGuid); - for (uint32_t stolenId : stolenIds) { + for (uint32_t stolenId : loggedIds) { addCombatText(CombatTextEntry::STEAL, 0, stolenId, isPlayerCaster, 0, stealCaster, stealVictim); } @@ -14080,6 +14103,31 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint combatLog_.push_back(std::move(log)); } +bool GameHandler::shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId) { + if (spellId == 0) return false; + + const auto now = std::chrono::steady_clock::now(); + constexpr auto kRecentWindow = std::chrono::seconds(1); + while (!recentSpellstealLogs_.empty() && + now - recentSpellstealLogs_.front().timestamp > kRecentWindow) { + recentSpellstealLogs_.pop_front(); + } + + for (auto it = recentSpellstealLogs_.begin(); it != recentSpellstealLogs_.end(); ++it) { + if (it->casterGuid == casterGuid && + it->victimGuid == victimGuid && + it->spellId == spellId) { + recentSpellstealLogs_.erase(it); + return false; + } + } + + if (recentSpellstealLogs_.size() >= MAX_RECENT_SPELLSTEAL_LOGS) + recentSpellstealLogs_.pop_front(); + recentSpellstealLogs_.push_back({casterGuid, victimGuid, spellId, now}); + return true; +} + void GameHandler::updateCombatText(float deltaTime) { for (auto& entry : combatText) { entry.age += deltaTime;