Files
Questie-X/Modules/Tooltips/TooltipHandler.lua
T
Xurkon 5b576911d7 fix: tooltip flicker, learner quest field mapping, secondary tooltip style
- QuestieLib.GetColoredQuestName now nil-guards QuestieDB.GetQuest() before reading .isComplete
- TooltipHandler.AddItemDataToTooltip re-keyed on itemId only (was firing on every ElvUI refresh)
- Removed unconditional ResizeTooltip call from item tooltip path (caused first-hover pop)
- OnQuestDetail no longer writes objectives text to data[6] or quest body to data[17] or zoneId to data[8]
- OnQuestAccepted documents why requiredLevel/requiredRaces/requiredClasses are intentionally not captured
- Reused logIdx from data build pass instead of redeclaring it for the objective scan
- Secondary learner tooltip now matches GameTooltip style with or without ElvUI (defers to ElvUI when loaded, replicates its Transparent template otherwise)
- Combined-tooltip learner stats now framed by invisible spacer lines for visual separation
2026-06-07 08:38:12 -05:00

445 lines
15 KiB
Lua

---@type QuestieTooltips
local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips");
local _QuestieTooltips = QuestieTooltips.private
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
--- COMPATIBILITY ---
local UnitGUID = QuestieCompat.UnitGUID
local lastGuid
-- ============================================================
-- NPCs that DROP an item which STARTS a quest (quest-starter drops)
-- Shows in tooltip like:
-- (yellow quest icon) Drops quest item !
-- (item icon) [ItemLink (quality colored)] [ID: <QuestId> (light blue)]
-- Works with Questie-335 compiled databases (binary + pointers).
-- ============================================================
local QUEST_START_LINE = "|TInterface\\GossipFrame\\AvailableQuestIcon:18:18:0:0|t |cFFFFD200Drops a quest !|r"
local QUEST_ID_COLOR = "|cFF80C8FF" -- light blue
local RESET_COLOR = "|r"
local _npcQuestStarterDrops = nil
local _npcQuestStarterDropsBuilt = false
-- ============================================================
-- Quest state cache (so we don't scan the log for every tooltip)
-- Hide lines if:
-- - quest is in quest log
-- - quest is flagged completed (turned in)
-- ============================================================
local _questInLogCache = {}
local _questInLogCacheBuilt = false
local function _WipeTable(t)
if wipe then
wipe(t)
else
for k in next, t do
t[k] = nil
end
end
end
local function _RebuildQuestInLogCache()
_WipeTable(_questInLogCache)
_questInLogCacheBuilt = true
if not GetNumQuestLogEntries or not GetQuestLogTitle then
return
end
local n = GetNumQuestLogEntries()
for i = 1, n do
-- WotLK: title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency, questID
local _, _, _, isHeader, _, _, _, questID = GetQuestLogTitle(i)
if not isHeader and questID then
questID = tonumber(questID)
if questID then
_questInLogCache[questID] = true
end
end
end
end
local function _QuestInLog(questId)
if not questId then return false end
if not _questInLogCacheBuilt then
_RebuildQuestInLogCache()
end
return _questInLogCache[questId] == true
end
-- Invalidate cache on quest log changes
do
local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_LOG_UPDATE")
f:RegisterEvent("QUEST_ACCEPTED")
f:RegisterEvent("QUEST_REMOVED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:SetScript("OnEvent", function()
_questInLogCacheBuilt = false
end)
end
local function _GetItemPointers()
-- QuestieDB exposes these after DB init:
-- QuestieDB.ItemPointers = QuestieDB.QueryItem.pointers
if QuestieDB and type(QuestieDB.ItemPointers) == "table" then
return QuestieDB.ItemPointers
end
if QuestieDB and QuestieDB.QueryItem and type(QuestieDB.QueryItem.pointers) == "table" then
return QuestieDB.QueryItem.pointers
end
return nil
end
-- Check if player already has the quest (in log or turned in)
local function _PlayerHasQuest(questId)
questId = tonumber(questId)
if not questId then return false end
-- 1) Active in log
if _QuestInLog(questId) then
return true
end
-- 2) Turned in / completed
if IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId) and not QuestiePlayer.currentQuestlog[questId] then
return true
end
-- Optional fallback (some cores implement this reliably)
if GetQuestLogIndexByID then
local idx = GetQuestLogIndexByID(questId)
if idx and idx > 0 then
return true
end
end
return false
end
local function _TryBuildNpcQuestStarterDrops()
if _npcQuestStarterDropsBuilt and _npcQuestStarterDrops then
return true
end
-- DB not ready yet? Try again later.
if not QuestieDB or type(QuestieDB.QueryItemSingle) ~= "function" then
return false
end
local pointers = _GetItemPointers()
if type(pointers) ~= "table" then
return false
end
_npcQuestStarterDrops = {}
-- Helper function to process an item and add it to the lookup table
local function processItem(itemId)
local questId = QuestieDB.QueryItemSingle(itemId, "startQuest")
if questId and questId ~= 0 then
local npcDrops = QuestieDB.QueryItemSingle(itemId, "npcDrops")
if npcDrops and type(npcDrops) == "table" then
local itemName = QuestieDB.QueryItemSingle(itemId, "name")
for _, npcId in next, npcDrops do
local list = _npcQuestStarterDrops[npcId]
if not list then
list = {}
_npcQuestStarterDrops[npcId] = list
end
list[table.getn(list) + 1] = { itemId = itemId, questId = tonumber(questId), name = itemName }
end
end
end
end
-- Iterate all items from compiled database
for itemId, _ in next, pointers do
processItem(itemId)
end
-- Also iterate Ascension override items (they don't have pointers)
if QuestieDB.itemDataOverrides and type(QuestieDB.itemDataOverrides) == "table" then
for itemId, _ in next, QuestieDB.itemDataOverrides do
processItem(itemId)
end
end
_npcQuestStarterDropsBuilt = true
return true
end
local function _TooltipHasQuestStarterLine(tooltip)
local n = tooltip:NumLines()
local base = tooltip:GetName() .. "TextLeft"
for i = 1, n do
local left = _G[base .. i]
if left then
local t = left:GetText()
if t and t:find("Drops quest item", 1, true) then
return true
end
end
end
return false
end
local function _AddQuestStarterDropsToTooltip(npcId)
if not _TryBuildNpcQuestStarterDrops() then return end
if not _npcQuestStarterDrops then return end
local drops = _npcQuestStarterDrops[npcId]
if not drops or table.getn(drops) == 0 then return end
if _TooltipHasQuestStarterLine(GameTooltip) then
return
end
-- Filter drops to only show items where player doesn't have the quest yet
local filteredDrops = {}
local dropsCount = table.getn(drops)
for i = 1, dropsCount do
local info = drops[i]
if info.questId and (not _PlayerHasQuest(info.questId)) then
filteredDrops[table.getn(filteredDrops) + 1] = info
end
end
if table.getn(filteredDrops) == 0 then
return
end
GameTooltip:AddLine(QUEST_START_LINE)
local filteredCount = table.getn(filteredDrops)
for i = 1, filteredCount do
local info = filteredDrops[i]
local itemId = info.itemId
local questId = info.questId
-- Item link with correct quality color when cached by client
local itemLink = select(2, GetItemInfo(itemId))
if not itemLink then
local itemName = info.name
if not itemName or itemName == "" then
itemName = "Item " .. tostring(itemId)
end
itemLink = ("|Hitem:%d:::::::::|h[%s]|h"):format(itemId, itemName)
end
local icon = GetItemIcon and GetItemIcon(itemId)
local qid = ("%s[ID: %d]%s"):format(QUEST_ID_COLOR, questId, RESET_COLOR)
if icon then
GameTooltip:AddLine(("|T%s:14:14:0:0|t %s %s"):format(icon, itemLink, qid))
else
GameTooltip:AddLine(("%s %s"):format(itemLink, qid))
end
end
end
function _QuestieTooltips:HideAscensionQuestLines(tooltip)
if not Questie.db.profile.enableTooltips then return end
local numLines = tooltip:NumLines()
if not numLines or numLines < 1 then return end
local questBlockActive = false
for i = 2, numLines do
local fontString = _G[tooltip:GetName() .. "TextLeft" .. i]
if fontString then
local text = fontString:GetText()
if text then
local cleanText = string.gsub(text, "|[cC]%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|[rR]", "")
cleanText = string.match(cleanText, "^%s*(.-)%s*$") or cleanText
-- Optional [-] or bullets in front of objective lines
if string.match(cleanText, "^%[%d+.-%]") or string.match(cleanText, "^%d+/%d+") or string.match(cleanText, "^%-.-%d+/%d+") then
fontString:SetText("")
fontString:Hide()
questBlockActive = true
elseif questBlockActive then
if string.match(text, "^%s+") then
questBlockActive = false
else
fontString:SetText("")
fontString:Hide()
end
end
end
end
end
end
function _QuestieTooltips:AddUnitDataToTooltip()
if (self.IsForbidden and self:IsForbidden()) or (not Questie.db.profile.enableTooltips) then
return
end
_QuestieTooltips:HideAscensionQuestLines(self)
local name, unitToken = self:GetUnit();
if not unitToken then return end
local guid = UnitGUID(unitToken);
if (not guid) then
guid = UnitGUID("mouseover");
end
local guidType, _, _, _, _, npcId, _ = strsplit("-", guid or "");
if name and (guidType == "Creature" or guidType == "Vehicle") and (
name ~= QuestieTooltips.lastGametooltipUnit or
(not QuestieTooltips.lastGametooltipCount) or
_QuestieTooltips:CountTooltip() < QuestieTooltips.lastGametooltipCount or
QuestieTooltips.lastGametooltipType ~= "monster" or
lastGuid ~= guid
) then
QuestieTooltips.lastGametooltipUnit = name
local tooltipData = QuestieTooltips:GetTooltip("m_" .. npcId);
if tooltipData then
if Questie.db.profile.enableTooltipsNPCID == true then
GameTooltip:AddDoubleLine("NPC ID", "|cFFFFFFFF" .. npcId .. "|r")
end
for _, v in next, tooltipData do
GameTooltip:AddLine(v)
end
else
-- Even if Questie has no objective tooltip for this NPC, we still want to show quest-starter drops.
if Questie.db.profile.enableTooltipsNPCID == true then
GameTooltip:AddDoubleLine("NPC ID", "|cFFFFFFFF" .. npcId .. "|r")
end
end
local npcNum = tonumber(npcId)
if npcNum then
_AddQuestStarterDropsToTooltip(npcNum)
end
if QuestieTooltips.ResizeTooltip then
QuestieTooltips:ResizeTooltip(GameTooltip)
end
QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip()
end
lastGuid = guid;
QuestieTooltips.lastGametooltipType = "monster";
end
-- =======================
-- Rest of original file
-- =======================
local lastItemId = 0;
function _QuestieTooltips:AddItemDataToTooltip()
if (self.IsForbidden and self:IsForbidden()) or (not Questie.db.profile.enableTooltips) then
return
end
_QuestieTooltips:HideAscensionQuestLines(self)
local name, link = self:GetItem()
local itemId
if link then
itemId = select(3,
string.match(link,
"|?c?f?f?(%x*)|?H?([^:]*):?(%d+):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%-?%d*):?(%-?%d*):?(%d*):?(%d*):?(%-?%d*)|?h?%[?([^%[%]]*)%]?|?h?|?r?"))
end
if name and itemId and (lastItemId ~= itemId) then
QuestieTooltips.lastGametooltipItem = name
local tooltipData = QuestieTooltips:GetTooltip("i_" .. (itemId or 0));
if tooltipData then
if Questie.db.profile.enableTooltipsItemID == true then
GameTooltip:AddDoubleLine("Item ID", "|cFFFFFFFF" .. itemId .. "|r")
end
for _, v in next, tooltipData do
self:AddLine(v)
end
end
QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip()
end
lastItemId = itemId;
QuestieTooltips.lastGametooltipType = "item";
QuestieTooltips.lastFrameName = self:GetName();
end
function _QuestieTooltips:AddObjectDataToTooltip(name)
if (not Questie.db.profile.enableTooltips) then
return
end
_QuestieTooltips:HideAscensionQuestLines(GameTooltip)
if name then
local titleAdded = false
local lookup = l10n.objectNameLookup[name] or {}
local count = table.getn(lookup)
if Questie.db.profile.enableTooltipsObjectID == true and count ~= 0 then
if count == 1 then
GameTooltip:AddDoubleLine("Object ID", "|cFFFFFFFF" .. lookup[1] .. "|r")
else
GameTooltip:AddDoubleLine("Object ID", "|cFFFFFFFF" .. lookup[1] .. " (" .. count .. ")|r")
end
end
local alreadyAddedObjectiveLines = {}
for _, gameObjectId in next, lookup do
local tooltipData = QuestieTooltips:GetTooltip("o_" .. gameObjectId);
if type(gameObjectId) == "number" and tooltipData then
if (not titleAdded) then
GameTooltip:AddLine(tooltipData[1])
titleAdded = true
end
if tooltipData[2] then
-- Quest has objectives
for index, line in next, tooltipData do
if index > 1 and (not alreadyAddedObjectiveLines[line]) then -- skip the first entry, it's the title
local _, _, acquired, needed = string.find(line, "(%d+)/(%d+)")
-- We need "tonumber", because acquired can contain parts of the color string
if acquired and tonumber(acquired) == tonumber(needed) then
-- We don't want to show completed objectives on game objects
break;
end
alreadyAddedObjectiveLines[line] = true
GameTooltip:AddLine(line)
end
end
end
end
end
if QuestieTooltips.ResizeTooltip then
QuestieTooltips:ResizeTooltip(GameTooltip)
end
GameTooltip:Show()
end
QuestieTooltips.lastGametooltipType = "object";
end
function _QuestieTooltips:CountTooltip()
local tooltipCount = 0
for i = 1, GameTooltip:NumLines() do
local frame = _G["GameTooltipTextLeft" .. i]
if frame and frame:GetText() then
tooltipCount = tooltipCount + 1
else
return tooltipCount
end
end
return tooltipCount
end