From b0b47c354a009f538cfd73e05115bc5e039c40a1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 11:11:33 -0700 Subject: [PATCH] feat: parse and display item skill/reputation requirements in tooltips - Store requiredSkill, requiredSkillRank, allowableClass, allowableRace, requiredReputationFaction, and requiredReputationRank from SMSG_ITEM_QUERY_SINGLE_RESPONSE in ItemQueryResponseData (was discarded) - Show "Requires ()" in item tooltip, highlighted red when the player doesn't have sufficient skill level - Show "Requires with " for reputation-gated items - Skill names resolved from SkillLine.dbc; faction names from Faction.dbc - Also fix loot window tooltip suppressing items with names starting with 'I' --- include/game/world_packets.hpp | 7 +++ src/game/packet_parsers_classic.cpp | 12 ++--- src/game/packet_parsers_tbc.cpp | 12 ++--- src/game/world_packets.cpp | 12 ++--- src/ui/game_screen.cpp | 5 ++- src/ui/inventory_screen.cpp | 68 +++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 20 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 7e0d9a41..4beab4a3 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1619,6 +1619,13 @@ struct ItemQueryResponseData { std::array socketColor{}; uint32_t socketBonus = 0; // enchantmentId of socket bonus; 0=none uint32_t itemSetId = 0; // ItemSet.dbc entry; 0=not part of a set + // Requirement fields + uint32_t requiredSkill = 0; // SkillLine.dbc ID (0 = no skill required) + uint32_t requiredSkillRank = 0; // Minimum skill value + uint32_t allowableClass = 0; // Class bitmask (0 = all classes) + uint32_t allowableRace = 0; // Race bitmask (0 = all races) + uint32_t requiredReputationFaction = 0; // Faction.dbc ID (0 = none) + uint32_t requiredReputationRank = 0; // 0=Hated..8=Exalted bool valid = false; }; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index c62567ef..acb198f6 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1394,17 +1394,17 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank packet.readUInt32(); // MaxCount data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index ffc462ad..8bda9afb 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1011,17 +1011,17 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank packet.readUInt32(); // MaxCount data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 090ead75..399c9fbd 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2868,17 +2868,17 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank packet.readUInt32(); // MaxCount data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 09ba2a91..6fd071d5 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -12462,8 +12462,9 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { // Show item tooltip on hover if (hovered && info && info->valid) { inventoryScreen.renderItemTooltip(*info); - } else if (hovered && !itemName.empty() && itemName[0] != 'I') { - ImGui::SetTooltip("%s", itemName.c_str()); + } else if (hovered && info && !info->name.empty()) { + // Item info received but not yet fully valid — show name at minimum + ImGui::SetTooltip("%s", info->name.c_str()); } ImDrawList* drawList = ImGui::GetWindowDrawList(); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 42090f2a..88e65163 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -2795,6 +2795,74 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, ImGui::TextColored(reqColor, "Requires Level %u", info.requiredLevel); } + // Required skill (e.g. "Requires Engineering (300)") + if (info.requiredSkill != 0 && info.requiredSkillRank > 0) { + // Lazy-load SkillLine.dbc names + static std::unordered_map s_skillNames; + static bool s_skillNamesLoaded = false; + if (!s_skillNamesLoaded && assetManager_) { + s_skillNamesLoaded = true; + auto dbc = assetManager_->loadDBC("SkillLine.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 2; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t sid = dbc->getUInt32(r, idF); + if (!sid) continue; + std::string sname = dbc->getString(r, nameF); + if (!sname.empty()) s_skillNames[sid] = std::move(sname); + } + } + } + uint32_t playerSkillVal = 0; + if (gameHandler_) { + const auto& skills = gameHandler_->getPlayerSkills(); + auto skPit = skills.find(info.requiredSkill); + if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue(); + } + bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info.requiredSkillRank); + ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + auto skIt = s_skillNames.find(info.requiredSkill); + if (skIt != s_skillNames.end()) + ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info.requiredSkillRank); + else + ImGui::TextColored(skColor, "Requires Skill %u (%u)", info.requiredSkill, info.requiredSkillRank); + } + + // Required reputation (e.g. "Requires Exalted with Argent Dawn") + if (info.requiredReputationFaction != 0 && info.requiredReputationRank > 0) { + static std::unordered_map s_factionNames; + static bool s_factionNamesLoaded = false; + if (!s_factionNamesLoaded && assetManager_) { + s_factionNamesLoaded = true; + auto dbc = assetManager_->loadDBC("Faction.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 20; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t fid = dbc->getUInt32(r, idF); + if (!fid) continue; + std::string fname = dbc->getString(r, nameF); + if (!fname.empty()) s_factionNames[fid] = std::move(fname); + } + } + } + static const char* kRepRankNames[] = { + "Hated", "Hostile", "Unfriendly", "Neutral", + "Friendly", "Honored", "Revered", "Exalted" + }; + const char* rankName = (info.requiredReputationRank < 8) + ? kRepRankNames[info.requiredReputationRank] : "Unknown"; + auto fIt = s_factionNames.find(info.requiredReputationFaction); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s", + rankName, + fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction"); + } + // Spell effects for (const auto& sp : info.spells) { if (sp.spellId == 0) continue;