Files
Questie-X/Modules/Tooltips/TooltipHandler.lua
T
Xurkon 49bc586c15 fix(tooltip): make Ascension quest-line stripping opt-in (#16)
HideAscensionQuestLines ran on every tooltip and removed any line
matching a quest-objective pattern (N/M, [N] ...), clobbering other
tooltip addons' lines (durability, stack counts, etc.). Gate it behind a
new General-tab option 'Hide Ascension quest progress lines' (default
off) so Questie no longer modifies tooltip lines it does not own by
default. Existing installs read the unset option as off automatically.
2026-06-10 20:39:21 -05:00

481 lines
18 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")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
--- 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
-- Opt-in only. This strips lines matching quest-objective patterns ("N/M", "[N] ...")
-- from tooltips to hide Ascension's server-injected quest progress. It runs on every
-- tooltip, so by default it must NOT touch them — otherwise it clobbers other tooltip
-- addons' lines that happen to look like "N/M" (durability, stack counts, etc.). Users
-- who want the Ascension quest-spam hidden can enable it explicitly. (#16)
if not Questie.db.profile.hideAscensionTooltipQuestLines then return end
local numLines = tooltip:NumLines()
if not numLines or numLines < 1 then return end
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
-- Hide ONLY the Ascension-injected objective progress lines themselves
-- (e.g. "0/8 Arcane Wraith slain", "[1] ...", "- 0/2 ...").
--
-- We intentionally do NOT hide the lines that follow. The previous
-- behavior set a "questBlockActive" flag and then wiped every later
-- non-indented line until it hit an indented one — which also removed
-- Questie's own "Item ID"/"NPC ID"/"Object ID" lines and any other
-- addon's tooltip additions (e.g. an item-count overlay), because those
-- are appended at the BOTTOM of the tooltip, after the objective block.
-- Matching each objective line individually removes the Ascension spam
-- without touching anything else on the tooltip.
if string.match(cleanText, "^%[%d+.-%]") or string.match(cleanText, "^%d+/%d+") or string.match(cleanText, "^%-.-%d+/%d+") then
fontString:SetText("")
fontString:Hide()
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
-- Suppress System A's inline learner lines here: QuestieLearner's OnTooltipSetUnit
-- hook already renders learner spawn/kill data for the hovered unit (into the main
-- tooltip, or the separate secondary learner frame when that option is enabled).
local tooltipData = QuestieTooltips:GetTooltip("m_" .. npcId, true);
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
-- Data-source attribution is rendered by QuestieLearner's secondary tooltip frame
-- (only when the secondary learner tooltip is enabled), never inline in the main
-- tooltip — see _AddLearnedSpawnTooltipLine.
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));
-- Item ID is shown unconditionally for every item hover (matches the
-- behavior of NPC/Object tooltips), so users can always copy/paste
-- the ID even for items that have no quest objective or starter
-- data attached. The line is always added when the itemId changes
-- regardless of whether tooltipData is non-nil.
if Questie.db.profile.enableTooltipsItemID == true then
self:AddDoubleLine("Item ID", "|cFFFFFFFF" .. itemId .. "|r")
end
-- If the item starts a quest (the player must right-click/use it,
-- or talk to an NPC while it's in their bag), surface that so the
-- player can see which quest the item unlocks without first
-- having to look it up. We never knew the item's startQuest
-- until runtime, so this is read directly from the static DB /
-- Ascension override table.
local startQuestId = QuestieDB and QuestieDB.QueryItemSingle
and tonumber(QuestieDB.QueryItemSingle(itemId, "startQuest") or 0) or 0
if startQuestId and startQuestId > 0 and (not _PlayerHasQuest(startQuestId)) then
local questTitle = QuestieLib and QuestieLib.GetColoredQuestName
and QuestieLib:GetColoredQuestName(startQuestId, Questie.db.profile.enableTooltipsQuestLevel, true, true)
or nil
if questTitle and questTitle ~= "" then
self:AddDoubleLine(QUEST_START_LINE, questTitle)
end
end
if tooltipData then
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