From 2de06c267a26eaa62078b98a4e4d628b65b02fd7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 21 Jul 2026 02:23:51 -0700 Subject: [PATCH] add map trackers and fix spell and mount state --- CMakeLists.txt | 3 + include/game/spell_classification.hpp | 7 +- include/rendering/world_map.hpp | 1 + .../world_map/layers/chest_tracker_layer.hpp | 22 +++++ .../rendering/world_map/world_map_facade.hpp | 2 + .../rendering/world_map/world_map_types.hpp | 7 ++ include/ui/settings_panel.hpp | 3 +- src/game/entity_controller.cpp | 14 +--- src/game/movement_handler.cpp | 11 +-- .../world_map/layers/chest_tracker_layer.cpp | 69 ++++++++++++++++ src/rendering/world_map/world_map_facade.cpp | 13 +++ src/ui/game_screen_hud.cpp | 22 +++++ src/ui/game_screen_minimap.cpp | 81 +++++++++++++++++++ src/ui/settings_panel.cpp | 8 +- tests/test_spell_classification.cpp | 21 +++++ 15 files changed, 261 insertions(+), 23 deletions(-) create mode 100644 include/rendering/world_map/layers/chest_tracker_layer.hpp create mode 100644 src/rendering/world_map/layers/chest_tracker_layer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ac12dc41..ca3a3d17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -808,6 +808,7 @@ set(WOWEE_SOURCES src/rendering/world_map/layers/quest_poi_layer.cpp src/rendering/world_map/layers/corpse_marker_layer.cpp src/rendering/world_map/layers/rare_tracker_layer.cpp + src/rendering/world_map/layers/chest_tracker_layer.cpp src/rendering/world_map/layers/zone_highlight_layer.cpp src/rendering/world_map/layers/coordinate_display.cpp src/rendering/world_map/layers/subzone_tooltip_layer.cpp @@ -996,6 +997,8 @@ set(WOWEE_HEADERS include/rendering/world_map/layers/poi_marker_layer.hpp include/rendering/world_map/layers/quest_poi_layer.hpp include/rendering/world_map/layers/corpse_marker_layer.hpp + include/rendering/world_map/layers/rare_tracker_layer.hpp + include/rendering/world_map/layers/chest_tracker_layer.hpp include/rendering/world_map/layers/zone_highlight_layer.hpp include/rendering/world_map/layers/coordinate_display.hpp include/rendering/world_map/layers/subzone_tooltip_layer.hpp diff --git a/include/game/spell_classification.hpp b/include/game/spell_classification.hpp index d3623ee0..1c27510b 100644 --- a/include/game/spell_classification.hpp +++ b/include/game/spell_classification.hpp @@ -134,11 +134,16 @@ inline uint32_t resolveHighestKnownRank( const SpellRankInfo* info = lookup(spellId); if (!info || info->name.empty()) return spellId; + // Keep the requested name by value. Some DBC adapters reuse one scratch object + // for every lookup; retaining its pointer here lets the first candidate overwrite + // the requested spell and makes unrelated known spells look like matching ranks. + const std::string requestedName = info->name; + uint32_t best = 0; int bestRank = -1; for (uint32_t known : knownSpells) { const SpellRankInfo* candidate = lookup(known); - if (!candidate || candidate->name != info->name) continue; + if (!candidate || candidate->name != requestedName) continue; const int rank = rankValue(candidate->rank); if (rank > bestRank) { bestRank = rank; diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index 3d2c2283..f08acfc7 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -12,6 +12,7 @@ namespace rendering { // (game_screen_hud.cpp, renderer.cpp, etc.) using WorldMapPartyDot = world_map::PartyDot; using WorldMapRareMark = world_map::RareMark; +using WorldMapChestMark = world_map::ChestMark; using WorldMapTaxiNode = world_map::TaxiNode; using MapPOI = world_map::POI; diff --git a/include/rendering/world_map/layers/chest_tracker_layer.hpp b/include/rendering/world_map/layers/chest_tracker_layer.hpp new file mode 100644 index 00000000..ee1bfcd0 --- /dev/null +++ b/include/rendering/world_map/layers/chest_tracker_layer.hpp @@ -0,0 +1,22 @@ +// chest_tracker_layer.hpp — Nearby spawned chest markers on the world map. +#pragma once +#include "rendering/world_map/overlay_renderer.hpp" +#include "rendering/world_map/world_map_types.hpp" +#include + +namespace wowee { +namespace rendering { +namespace world_map { + +class ChestTrackerLayer : public IOverlayLayer { +public: + void setChests(const std::vector& chests) { chests_ = &chests; } + void render(const LayerContext& ctx) override; + +private: + const std::vector* chests_ = nullptr; +}; + +} // namespace world_map +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/world_map/world_map_facade.hpp b/include/rendering/world_map/world_map_facade.hpp index b4d7f3c3..79751992 100644 --- a/include/rendering/world_map/world_map_facade.hpp +++ b/include/rendering/world_map/world_map_facade.hpp @@ -47,6 +47,8 @@ public: void setCorpsePos(bool hasCorpse, glm::vec3 renderPos); /// Nearby rare/rare-elite creatures currently spawned near the player. void setRares(std::vector rares); + /// Nearby chest-type game objects currently spawned near the player. + void setChests(std::vector chests); bool isOpen() const; void close(); diff --git a/include/rendering/world_map/world_map_types.hpp b/include/rendering/world_map/world_map_types.hpp index 7eaec215..2d862af0 100644 --- a/include/rendering/world_map/world_map_types.hpp +++ b/include/rendering/world_map/world_map_types.hpp @@ -108,6 +108,13 @@ struct RareMark { int rank = 4; ///< Creature rank: 2 = Rare Elite, 4 = Rare }; +// ── Nearby spawned chest marker (UI layer → world map overlay) ─ + +struct ChestMark { + glm::vec3 renderPos; ///< Position in render-space coordinates + std::string name; ///< Chest/container name (shown as tooltip on hover) +}; + // ── Taxi (flight master) node (UI layer → world map overlay) ─ struct TaxiNode { diff --git a/include/ui/settings_panel.hpp b/include/ui/settings_panel.hpp index d2d4c357..8f3a9b01 100644 --- a/include/ui/settings_panel.hpp +++ b/include/ui/settings_panel.hpp @@ -157,7 +157,8 @@ public: bool showFriendlyNameplates_ = true; // Shift+V toggles friendly player nameplates bool showDPSMeter_ = false; bool showCooldownTracker_ = false; - bool showRareTracker_ = false; // Mark nearby spawned rares/rare-elites on the world map + bool showRareTracker_ = false; // Mark nearby spawned rares/rare-elites on both maps + bool showChestTracker_ = false; // Mark nearby spawned non-gather chests on both maps bool damageFlashEnabled_ = true; bool lowHealthVignetteEnabled_ = true; // Persistent pulsing red vignette below 20% HP diff --git a/src/game/entity_controller.cpp b/src/game/entity_controller.cpp index e9f96056..a1fc8704 100644 --- a/src/game/entity_controller.cpp +++ b/src/game/entity_controller.cpp @@ -1844,23 +1844,15 @@ void EntityController::onValuesUpdatePlayer(const UpdateBlock& block, std::share oldFieldsSnapshot = owner_.lastPlayerFieldsRef(); } if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { + // Speed is independent of mount ownership: slows and ordinary movement + // snapshots may report base-speed values while UNIT_FIELD_MOUNTDISPLAYID + // still authoritatively says the player is mounted. if (auto* movement = owner_.getMovementHandler()) { movement->applyServerMovementSpeeds( block.walkSpeed, block.runSpeed, block.runBackSpeed, block.swimSpeed, block.swimBackSpeed, block.flightSpeed, block.flightBackSpeed, block.turnRate, block.pitchRate); } - // Some server dismount paths update run speed without updating mount display field. - const bool onRealTaxiFlight = owner_.getMovementHandler() && owner_.getMovementHandler()->isOnTaxiFlight(); - if (!onRealTaxiFlight && !owner_.taxiMountActiveRef() && - owner_.currentMountDisplayIdRef() != 0 && block.runSpeed <= 8.5f) { - LOG_INFO("Auto-clearing mount from movement speed update: speed=", block.runSpeed, - " displayId=", owner_.currentMountDisplayIdRef()); - owner_.currentMountDisplayIdRef() = 0; - if (owner_.mountCallbackRef()) { - owner_.mountCallbackRef()(0); - } - } } // Merge block fields into the persistent snapshot. Both are sorted, so // a simple insert_or_assign per key keeps the invariant intact. diff --git a/src/game/movement_handler.cpp b/src/game/movement_handler.cpp index 5c85b17a..65ceb198 100644 --- a/src/game/movement_handler.cpp +++ b/src/game/movement_handler.cpp @@ -982,16 +982,9 @@ void MovementHandler::handleForceSpeedChange(network::Packet& packet, const char } void MovementHandler::handleForceRunSpeedChange(network::Packet& packet) { + // A speed change is not a dismount signal. Mount ownership is updated by + // SMSG_DISMOUNT / UNIT_FIELD_MOUNTDISPLAYID instead. handleForceSpeedChange(packet, "RUN_SPEED", Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, &serverRunSpeed_); - - if (!onTaxiFlight_ && !taxiMountActive_ && owner_.currentMountDisplayIdRef() != 0 && serverRunSpeed_ <= 8.5f) { - LOG_INFO("Auto-clearing mount from speed change: speed=", serverRunSpeed_, - " displayId=", owner_.currentMountDisplayIdRef()); - owner_.currentMountDisplayIdRef() = 0; - if (owner_.mountCallbackRef()) { - owner_.mountCallbackRef()(0); - } - } } void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) { diff --git a/src/rendering/world_map/layers/chest_tracker_layer.cpp b/src/rendering/world_map/layers/chest_tracker_layer.cpp new file mode 100644 index 00000000..06ee1e7f --- /dev/null +++ b/src/rendering/world_map/layers/chest_tracker_layer.cpp @@ -0,0 +1,69 @@ +// chest_tracker_layer.cpp — Nearby spawned chest markers on the world map. +#include "rendering/world_map/layers/chest_tracker_layer.hpp" +#include "rendering/world_map/coordinate_projection.hpp" +#include + +namespace wowee { +namespace rendering { +namespace world_map { + +void ChestTrackerLayer::render(const LayerContext& ctx) { + if (!chests_ || chests_->empty()) return; + if (ctx.currentZoneIdx < 0) return; + if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return; + if (!ctx.zones) return; + + const auto& zone = (*ctx.zones)[ctx.currentZoneIdx]; + ZoneBounds bounds = zone.bounds; + const bool isContinent = zone.areaID == 0; + if (isContinent) { + float left, right, top, bottom; + if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, + left, right, top, bottom)) { + bounds = {left, right, top, bottom}; + } + } + + constexpr ImU32 fill = IM_COL32(205, 125, 35, 255); + constexpr ImU32 outline = IM_COL32(45, 25, 5, 230); + ImFont* font = ImGui::GetFont(); + for (const auto& chest : *chests_) { + glm::vec2 uv = renderPosToMapUV(chest.renderPos, bounds, isContinent); + if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue; + const float px = ctx.imgMin.x + uv.x * ctx.displayW; + const float py = ctx.imgMin.y + uv.y * ctx.displayH; + + // Compact chest silhouette: box, domed lid line, and bright latch. + constexpr float halfW = 6.0f; + constexpr float halfH = 4.5f; + ctx.drawList->AddRectFilled(ImVec2(px - halfW, py - halfH), + ImVec2(px + halfW, py + halfH), fill, 1.5f); + ctx.drawList->AddRect(ImVec2(px - halfW, py - halfH), + ImVec2(px + halfW, py + halfH), outline, 1.5f, 0, 1.5f); + ctx.drawList->AddLine(ImVec2(px - halfW, py - 1.0f), + ImVec2(px + halfW, py - 1.0f), outline, 1.2f); + ctx.drawList->AddRectFilled(ImVec2(px - 1.2f, py - 1.5f), + ImVec2(px + 1.2f, py + 1.5f), + IM_COL32(255, 220, 80, 255), 0.5f); + + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x >= px - halfW - 2.0f && mouse.x <= px + halfW + 2.0f && + mouse.y >= py - halfH - 2.0f && mouse.y <= py + halfH + 2.0f && + !chest.name.empty()) { + ImGui::SetTooltip("%s\nChest", chest.name.c_str()); + } + if (!chest.name.empty()) { + ImVec2 nameSize = font->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, + chest.name.c_str()); + const float tx = px - nameSize.x * 0.5f; + const float ty = py - nameSize.y - halfH - 3.0f; + ctx.drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), + IM_COL32(0, 0, 0, 190), chest.name.c_str()); + ctx.drawList->AddText(ImVec2(tx, ty), fill, chest.name.c_str()); + } + } +} + +} // namespace world_map +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/world_map/world_map_facade.cpp b/src/rendering/world_map/world_map_facade.cpp index 594c2aee..c7ce2215 100644 --- a/src/rendering/world_map/world_map_facade.cpp +++ b/src/rendering/world_map/world_map_facade.cpp @@ -17,6 +17,7 @@ #include "rendering/world_map/layers/quest_poi_layer.hpp" #include "rendering/world_map/layers/corpse_marker_layer.hpp" #include "rendering/world_map/layers/rare_tracker_layer.hpp" +#include "rendering/world_map/layers/chest_tracker_layer.hpp" #include "rendering/world_map/layers/zone_highlight_layer.hpp" #include "rendering/world_map/layers/coordinate_display.hpp" #include "rendering/world_map/layers/subzone_tooltip_layer.hpp" @@ -121,11 +122,13 @@ struct WorldMapFacade::Impl { // Typed layer pointers for setters (non-owning references into overlay) PartyDotLayer* partyDotLayer = nullptr; std::vector rares; + std::vector chests; TaxiNodeLayer* taxiNodeLayer = nullptr; POIMarkerLayer* poiMarkerLayer = nullptr; QuestPOILayer* questPOILayer = nullptr; CorpseMarkerLayer* corpseMarkerLayer = nullptr; RareTrackerLayer* rareTrackerLayer = nullptr; + ChestTrackerLayer* chestTrackerLayer = nullptr; ZoneHighlightLayer* zoneHighlightLayer = nullptr; PlayerMarkerLayer* playerMarkerLayer = nullptr; @@ -305,6 +308,11 @@ void WorldMapFacade::Impl::initOverlayLayers() { rareTrackerLayer = rtLayer.get(); overlay.addLayer(std::move(rtLayer)); + // Nearby spawned chest tracker + auto ctLayer = std::make_unique(); + chestTrackerLayer = ctLayer.get(); + overlay.addLayer(std::move(ctLayer)); + // Coordinate display overlay.addLayer(std::make_unique()); @@ -635,6 +643,10 @@ void WorldMapFacade::setRares(std::vector rares) { impl_->rares = std::move(rares); } +void WorldMapFacade::setChests(std::vector chests) { + impl_->chests = std::move(chests); +} + void WorldMapFacade::setTaxiNodes(std::vector nodes) { impl_->taxiNodes = std::move(nodes); } @@ -815,6 +827,7 @@ void WorldMapFacade::Impl::renderImGuiOverlay(const glm::vec3& playerRenderPos, // Update layer data pointers if (partyDotLayer) partyDotLayer->setDots(partyDots); if (rareTrackerLayer) rareTrackerLayer->setRares(rares); + if (chestTrackerLayer) chestTrackerLayer->setChests(chests); if (taxiNodeLayer) taxiNodeLayer->setNodes(taxiNodes); if (poiMarkerLayer) poiMarkerLayer->setMarkers(data.poiMarkers()); if (questPOILayer) questPOILayer->setPois(questPois); diff --git a/src/ui/game_screen_hud.cpp b/src/ui/game_screen_hud.cpp index a693bf06..565928d6 100644 --- a/src/ui/game_screen_hud.cpp +++ b/src/ui/game_screen_hud.cpp @@ -596,6 +596,28 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { wm->setRares(std::move(rares)); } + // Chest tracker: mark loaded type-3 chest objects while excluding mining and + // herbalism nodes, which share GAMEOBJECT_TYPE_CHEST on the wire. + { + std::vector chests; + if (settingsPanel_.showChestTracker_) { + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::GAMEOBJECT) continue; + auto chest = std::static_pointer_cast(entity); + const auto* info = gameHandler.getCachedGameObjectInfo(chest->getEntry()); + if (!info || !info->isValid() || info->type != 3) continue; + if (gameHandler.isGatherGameObject(guid)) continue; + + rendering::WorldMapChestMark mark; + mark.renderPos = core::coords::canonicalToRender( + glm::vec3(chest->getX(), chest->getY(), chest->getZ())); + mark.name = chest->getName().empty() ? info->name : chest->getName(); + chests.push_back(std::move(mark)); + } + } + wm->setChests(std::move(chests)); + } + glm::vec3 playerPos = renderer->getCharacterPosition(); float playerYaw = renderer->getCharacterYaw(); auto* window = app.getWindow(); diff --git a/src/ui/game_screen_minimap.cpp b/src/ui/game_screen_minimap.cpp index 13cec6ce..99f3cf06 100644 --- a/src/ui/game_screen_minimap.cpp +++ b/src/ui/game_screen_minimap.cpp @@ -257,6 +257,46 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } + // Rare tracker: use the same live creature-rank classification as the world map. + // This is independent of generic NPC dots so enabling rare tracking consistently + // shows spawned rares on both maps without adding every nearby creature. + if (settingsPanel_.showRareTracker_) { + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& entity : minimapUnits) { + auto unit = std::static_pointer_cast(entity); + if (!unit || unit->getHealth() == 0) continue; + + const int rank = gameHandler.getCreatureRank(unit->getEntry()); + if (rank != 2 && rank != 4) continue; // 2 = Rare Elite, 4 = Rare + + glm::vec3 rareRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(rareRender, sx, sy)) continue; + + // Match the world-map tracker: gold for Rare, silver for Rare Elite. + const bool isElite = rank == 2; + const ImU32 fill = isElite + ? IM_COL32(210, 210, 225, 255) + : IM_COL32(255, 190, 60, 255); + constexpr float halfSize = 5.0f; + const ImVec2 top(sx, sy - halfSize); + const ImVec2 right(sx + halfSize, sy); + const ImVec2 bottom(sx, sy + halfSize); + const ImVec2 left(sx - halfSize, sy); + drawList->AddQuadFilled(top, right, bottom, left, fill); + drawList->AddQuad(top, right, bottom, left, IM_COL32(0, 0, 0, 220), 1.5f); + + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy <= 49.0f) { + const std::string& name = unit->getName(); + ImGui::SetTooltip("%s\n%s", + name.empty() ? "Unknown creature" : name.c_str(), + isElite ? "Rare Elite" : "Rare"); + } + } + } + // Nearby other-player dots — shown when NPC dots are enabled. // Party members are already drawn as squares above; other players get a small circle. if (settingsPanel_.minimapNpcDots_) { @@ -366,6 +406,44 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } + // Chest tracker: GAMEOBJECT_TYPE_CHEST also covers gathering nodes on WoW + // servers, so explicitly exclude the existing mining/herbalism classification. + if (settingsPanel_.showChestTracker_) { + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& entity : minimapGameObjects) { + auto chest = std::static_pointer_cast(entity); + if (!chest) continue; + const auto* info = gameHandler.getCachedGameObjectInfo(chest->getEntry()); + if (!info || !info->isValid() || info->type != 3) continue; + if (gameHandler.isGatherGameObject(chest->getGuid())) continue; + + glm::vec3 chestRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(chestRender, sx, sy)) continue; + + constexpr float halfW = 5.5f; + constexpr float halfH = 4.0f; + constexpr ImU32 fill = IM_COL32(205, 125, 35, 255); + constexpr ImU32 outline = IM_COL32(45, 25, 5, 230); + drawList->AddRectFilled(ImVec2(sx - halfW, sy - halfH), + ImVec2(sx + halfW, sy + halfH), fill, 1.5f); + drawList->AddRect(ImVec2(sx - halfW, sy - halfH), + ImVec2(sx + halfW, sy + halfH), outline, 1.5f, 0, 1.5f); + drawList->AddLine(ImVec2(sx - halfW, sy - 0.8f), + ImVec2(sx + halfW, sy - 0.8f), outline, 1.0f); + drawList->AddRectFilled(ImVec2(sx - 1.0f, sy - 1.2f), + ImVec2(sx + 1.0f, sy + 1.3f), + IM_COL32(255, 220, 80, 255), 0.5f); + + if (mouse.x >= sx - halfW - 2.0f && mouse.x <= sx + halfW + 2.0f && + mouse.y >= sy - halfH - 2.0f && mouse.y <= sy + halfH + 2.0f) { + const std::string& name = chest->getName().empty() ? info->name : chest->getName(); + ImGui::SetTooltip("%s\nChest", name.empty() ? "Chest" : name.c_str()); + } + } + } + // Party member dots on minimap — small colored squares with name tooltip on hover if (gameHandler.isInGroup()) { const auto& partyData = gameHandler.getPartyData(); @@ -1455,6 +1533,7 @@ void GameScreen::saveSettings() { out << "show_dps_meter=" << (settingsPanel_.showDPSMeter_ ? 1 : 0) << "\n"; out << "show_cooldown_tracker=" << (settingsPanel_.showCooldownTracker_ ? 1 : 0) << "\n"; out << "show_rare_tracker=" << (settingsPanel_.showRareTracker_ ? 1 : 0) << "\n"; + out << "show_chest_tracker=" << (settingsPanel_.showChestTracker_ ? 1 : 0) << "\n"; out << "separate_bags=" << (settingsPanel_.pendingSeparateBags ? 1 : 0) << "\n"; out << "show_keyring=" << (settingsPanel_.pendingShowKeyring ? 1 : 0) << "\n"; out << "bag_scale=" << settingsPanel_.pendingBagScale << "\n"; @@ -1616,6 +1695,8 @@ void GameScreen::loadSettings() { settingsPanel_.showCooldownTracker_ = (std::stoi(val) != 0); } else if (key == "show_rare_tracker") { settingsPanel_.showRareTracker_ = (std::stoi(val) != 0); + } else if (key == "show_chest_tracker") { + settingsPanel_.showChestTracker_ = (std::stoi(val) != 0); } else if (key == "separate_bags") { settingsPanel_.pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(settingsPanel_.pendingSeparateBags); diff --git a/src/ui/settings_panel.cpp b/src/ui/settings_panel.cpp index 86e360bd..368bf13a 100644 --- a/src/ui/settings_panel.cpp +++ b/src/ui/settings_panel.cpp @@ -175,7 +175,13 @@ if (ImGui::Checkbox("Show Rare Tracker", &showRareTracker_)) { saveCallback(); } ImGui::SameLine(); -ImGui::TextDisabled("(mark nearby spawned rares on the world map)"); +ImGui::TextDisabled("(mark nearby spawned rares on the world map and minimap)"); + +if (ImGui::Checkbox("Show Chest Tracker", &showChestTracker_)) { + saveCallback(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(mark nearby spawned loot chests on the world map and minimap)"); ImGui::Spacing(); ImGui::SeparatorText("Screen Effects"); diff --git a/tests/test_spell_classification.cpp b/tests/test_spell_classification.cpp index 3307a18d..d30746f8 100644 --- a/tests/test_spell_classification.cpp +++ b/tests/test_spell_classification.cpp @@ -164,6 +164,27 @@ TEST_CASE("A superseded rank resolves to the highest known rank", "[spell][rank] REQUIRE(resolveHighestKnownRank(11549, known, dbc.lookup()) == 6192); } +TEST_CASE("Rank resolution tolerates a lookup adapter with reused storage", "[spell][rank]") { + const std::unordered_map entries{ + {6673, {"Battle Shout", "Rank 1"}}, + {5242, {"Battle Shout", "Rank 2"}}, + {6192, {"Battle Shout", "Rank 3"}}, + {133, {"Fireball", "Rank 14"}}, + }; + SpellRankInfo scratch; + auto reusedLookup = [&entries, &scratch](uint32_t id) -> const SpellRankInfo* { + auto it = entries.find(id); + if (it == entries.end()) return nullptr; + scratch = it->second; + return &scratch; + }; + + // The production Spell.dbc adapter reuses scratch storage. Looking up Fireball + // must not overwrite the requested Battle Shout name and win merely by rank. + const std::unordered_set known{5242, 6192, 133}; + REQUIRE(resolveHighestKnownRank(6673, known, reusedLookup) == 6192); +} + TEST_CASE("A known spell is left alone", "[spell][rank]") { FakeSpellDbc dbc; dbc.add(772, "Rend", "Rank 1");