From 55fd692c1a4cd0b3922632c9e76e578ee527941f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Feb 2026 15:49:12 -0800 Subject: [PATCH] Fix buff bar: opcode merge, isBuff flag, and duration countdown Root cause: OpcodeTable::loadFromJson() cleared all mappings before loading the expansion JSON, so any WotLK opcode absent from Turtle WoW's opcodes.json (including SMSG_AURA_UPDATE and SMSG_AURA_UPDATE_ALL) was permanently lost. Changed loadFromJson to patch/merge on top of existing defaults so only explicitly listed opcodes are overridden. Also fix isBuff border color: was testing flag 0x02 (effect 2 active) instead of 0x80 (negative/debuff flag). Add client-side duration countdown: AuraSlot.receivedAtMs is stamped when the packet arrives; getRemainingMs(nowMs) subtracts elapsed time so buff tooltips show accurate remaining duration instead of stale snapshot. --- include/game/spell_defines.hpp | 8 ++++++++ src/game/game_handler.cpp | 9 ++++++++- src/game/opcode_table.cpp | 12 ++++++++---- src/ui/game_screen.cpp | 12 ++++++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index cec57a52..874533f0 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -18,8 +18,16 @@ struct AuraSlot { int32_t durationMs = -1; int32_t maxDurationMs = -1; uint64_t casterGuid = 0; + uint64_t receivedAtMs = 0; // Client timestamp (ms) when durationMs was set bool isEmpty() const { return spellId == 0; } + // Remaining duration in ms, counting down from when the packet was received + int32_t getRemainingMs(uint64_t nowMs) const { + if (durationMs < 0) return -1; + uint64_t elapsed = (nowMs > receivedAtMs) ? (nowMs - receivedAtMs) : 0; + int32_t remaining = durationMs - static_cast(elapsed); + return (remaining > 0) ? remaining : 0; + } }; /** diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 037e643e..3f6c9e4a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7162,7 +7162,14 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { if (isAll) { auraList->clear(); } - for (const auto& [slot, aura] : data.updates) { + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + for (auto [slot, aura] : data.updates) { + // Stamp client timestamp so the UI can count down duration locally + if (aura.durationMs >= 0) { + aura.receivedAtMs = nowMs; + } // Ensure vector is large enough while (auraList->size() <= slot) { auraList->push_back(AuraSlot{}); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index f85a23bc..b6b78446 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -698,13 +698,12 @@ bool OpcodeTable::loadFromJson(const std::string& path) { std::string json((std::istreambuf_iterator(f)), std::istreambuf_iterator()); - // Save old tables so we can restore on failure + // Merge/patch on top of existing table (WotLK defaults must be loaded first). + // Opcodes NOT in the JSON keep their current mapping, so expansion-specific + // JSONs only need to list entries that differ from the WotLK baseline. auto savedLogicalToWire = logicalToWire_; auto savedWireToLogical = wireToLogical_; - logicalToWire_.clear(); - wireToLogical_.clear(); - // Parse simple JSON: { "NAME": "0xHEX", ... } or { "NAME": 123, ... } size_t pos = 0; size_t loaded = 0; @@ -746,6 +745,11 @@ bool OpcodeTable::loadFromJson(const std::string& path) { auto logOp = nameToLogical(key); if (logOp) { uint16_t logIdx = static_cast(*logOp); + // Remove stale reverse-mapping for this logical opcode's old wire value + auto oldIt = logicalToWire_.find(logIdx); + if (oldIt != logicalToWire_.end()) { + wireToLogical_.erase(oldIt->second); + } logicalToWire_[logIdx] = wire; wireToLogical_[wire] = logIdx; ++loaded; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 09e33e9d..4ffc9587 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4044,7 +4044,7 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImGui::PushID(static_cast(i)); - bool isBuff = (aura.flags & 0x02) != 0; + bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); // Try to get spell icon @@ -4078,12 +4078,16 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { } } - // Tooltip with spell name and duration + // Tooltip with spell name and live countdown if (ImGui::IsItemHovered()) { std::string name = spellbookScreen.lookupSpellName(aura.spellId, assetMgr); if (name.empty()) name = "Spell #" + std::to_string(aura.spellId); - if (aura.durationMs > 0) { - int seconds = aura.durationMs / 1000; + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + int32_t remaining = aura.getRemainingMs(nowMs); + if (remaining > 0) { + int seconds = remaining / 1000; if (seconds < 60) { ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds); } else {