Compare commits
20 Commits
c09d30b832
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fe4b83a9 | |||
| 26a135a2ca | |||
| cf67ceb474 | |||
| de270a0e8e | |||
| c6019ea823 | |||
| 706fc0b8ad | |||
| c2b1b82b2f | |||
| 7ecc9016dd | |||
| 0103932b9b | |||
| 6aafd2c112 | |||
| 9da949dd66 | |||
| 190d2b0d5e | |||
| 460718e313 | |||
| b4e0894c3f | |||
| 8089b5f08c | |||
| 75bebafaa9 | |||
| 6fad92244c | |||
| 62956abb8e | |||
| 13cddced16 | |||
| 93c5e56c1d |
@@ -1090,6 +1090,14 @@ local questTagToName = {
|
||||
[85] = "Heroic",
|
||||
}
|
||||
|
||||
-- Reverse lookup (tag name -> tag id), exposed so callers can convert the live
|
||||
-- questTag string returned by the game's own GetQuestLogTitle back into the
|
||||
-- numeric tag id used throughout Questie.
|
||||
QuestieCompat.QuestTagNameToId = {}
|
||||
for id, name in pairs(questTagToName) do
|
||||
QuestieCompat.QuestTagNameToId[name] = id
|
||||
end
|
||||
|
||||
-- Retrieves tag information about the quest.
|
||||
-- https://wowpedia.fandom.com/wiki/API_GetQuestTagInfo
|
||||
function QuestieCompat.GetQuestTagInfo(questId)
|
||||
|
||||
+10
-20
@@ -879,26 +879,16 @@ local function HandleWorldMapPin(icon, data)
|
||||
icon:SetPoint("CENTER", WorldMapButton, "TOPLEFT", (x * worldmapWidth) / iconEffScale, -(y * worldmapHeight) / iconEffScale)
|
||||
icon:Show()
|
||||
|
||||
-- Questie fix: when zoomed in (e.g. via Magnify/LootCollector), WorldMapButton is
|
||||
-- drawn far larger than the visible window and only WorldMapScrollFrame clips it.
|
||||
-- Pins positioned past the visible edge still render on top of the surrounding UI
|
||||
-- instead of being clipped, so hide them manually once they're outside the
|
||||
-- scroll frame's actual on-screen bounds. GetLeft/Right/Top/Bottom/Center are all
|
||||
-- in a shared scale-independent coordinate space, so no extra scale math needed.
|
||||
-- Only do this when the map is actually drawn larger than its viewport (the zoom
|
||||
-- case) -- at normal size (e.g. just the "show objective" panel active, no zoom)
|
||||
-- the whole map already fits on-screen, so this check should never fire there;
|
||||
-- skip it entirely rather than risk false positives hiding valid icons.
|
||||
if WorldMapScrollFrame and WorldMapScrollFrame:IsVisible() then
|
||||
local scrollWidth, scrollHeight = WorldMapScrollFrame:GetWidth(), WorldMapScrollFrame:GetHeight()
|
||||
if scrollWidth and scrollHeight and (worldmapWidth > scrollWidth + 1 or worldmapHeight > scrollHeight + 1) then
|
||||
local iconCenterX, iconCenterY = icon:GetCenter()
|
||||
local vLeft, vRight, vTop, vBottom = WorldMapScrollFrame:GetLeft(), WorldMapScrollFrame:GetRight(), WorldMapScrollFrame:GetTop(), WorldMapScrollFrame:GetBottom()
|
||||
if iconCenterX and vLeft and (iconCenterX < vLeft or iconCenterX > vRight or iconCenterY > vTop or iconCenterY < vBottom) then
|
||||
icon:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Manual GetLeft/Right/Top/Bottom-vs-GetCenter overflow culling used to live here
|
||||
-- (Questie fix for Magnify/LootCollector zoom). Removed: it compared Get*() values
|
||||
-- across frames with different effective scales (icon's scale is dragged around by
|
||||
-- Magnify's WorldMapDetailFrame:SetScale() zoom, WorldMapScrollFrame's isn't), which
|
||||
-- are only directly comparable at equal effective scale -- at high zoom ratios the
|
||||
-- mismatch made the check fail for every icon, hiding the entire map. Icons are
|
||||
-- already parented inside WorldMapButton, itself inside Magnify's real
|
||||
-- WorldMapScrollFrame:SetScrollChild() subtree once SetDrawOrder correctly detects
|
||||
-- Magnify (see Modules/Map/QuestieMapUtils.lua), so the engine's native scroll-child
|
||||
-- clip now handles this on all 4 edges, during pan, with no Lua-side math needed.
|
||||
else
|
||||
icon:Hide()
|
||||
end
|
||||
|
||||
+62
-5
@@ -999,6 +999,25 @@ end
|
||||
---@param questId number
|
||||
---@return number|nil questType, string|nil questTag
|
||||
function QuestieDB.GetQuestTagInfo(questId)
|
||||
-- Prefer the server's own live tag over static data whenever the quest is currently in
|
||||
-- the quest log. Custom servers can reuse a Blizzard quest ID for different content (e.g.
|
||||
-- Ascension's "Bride of the Embalmer" reusing ID 253, which Questie's static QuestTag.lua
|
||||
-- correctly marks as non-group for retail's real quest 253), which the static tables below
|
||||
-- have no way to know about. The live tag is authoritative once available, whether that
|
||||
-- means it HAS a tag the static data misses, or has NO tag the static data wrongly assumes.
|
||||
local cachedQuest = QuestLogCache.questLog_DO_NOT_MODIFY[questId]
|
||||
if cachedQuest then
|
||||
local liveTag = cachedQuest.questTag
|
||||
if liveTag then
|
||||
local liveTagId = QuestieCompat.QuestTagNameToId[liveTag]
|
||||
if liveTagId then
|
||||
return liveTagId, liveTag
|
||||
end
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
if questTagCorrections[questId] then
|
||||
return questTagCorrections[questId][1], questTagCorrections[questId][2]
|
||||
end
|
||||
@@ -1766,16 +1785,36 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
-- Build a minimal live-fallback quest from the quest log so the tracker still works
|
||||
-- Build a minimal live-fallback quest from the quest log so the tracker still works.
|
||||
-- The override that got us here is partial by nature -- a Learner record of a single
|
||||
-- field, a correction -- but whatever it does carry beats guessing, and the quest object
|
||||
-- built here is cached for the session, so anything left blank stays blank.
|
||||
local logEntry = QuestLogCache.GetQuest(questId)
|
||||
if not logEntry then return nil end
|
||||
local function NonEmpty(text)
|
||||
if text and text ~= "" then return text end
|
||||
return nil
|
||||
end
|
||||
local overrideName = NonEmpty(overrideData[QuestieDB.questKeys.name])
|
||||
local overrideLevel = overrideData[QuestieDB.questKeys.questLevel]
|
||||
local overrideZone = overrideData[QuestieDB.questKeys.zoneOrSort]
|
||||
local cachedTitle = NonEmpty(logEntry.title)
|
||||
local liveTitle
|
||||
if (not overrideName) and (not cachedTitle) and GetQuestLogIndexByID and GetQuestLogTitle then
|
||||
-- QuestLogCache is a snapshot and can be missing the title of a quest the client
|
||||
-- has since filled in, which is what leaves a quest showing as its own id.
|
||||
local questLogIndex = GetQuestLogIndexByID(questId)
|
||||
if questLogIndex and questLogIndex > 0 then
|
||||
liveTitle = NonEmpty(GetQuestLogTitle(questLogIndex))
|
||||
end
|
||||
end
|
||||
local fallback = {
|
||||
Id = questId,
|
||||
name = logEntry.title or tostring(questId),
|
||||
level = logEntry.level or 0,
|
||||
questLevel = logEntry.level or 0,
|
||||
name = overrideName or cachedTitle or liveTitle or tostring(questId),
|
||||
level = overrideLevel or logEntry.level or 0,
|
||||
questLevel = overrideLevel or logEntry.level or 0,
|
||||
requiredLevel = 0,
|
||||
zoneOrSort = 0,
|
||||
zoneOrSort = overrideZone or 0,
|
||||
questFlags = 0,
|
||||
specialFlags = 0,
|
||||
Starts = { CreatureStarts = {}, ObjectStarts = {}, ItemStarts = {} },
|
||||
@@ -1897,6 +1936,24 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
||||
if learnerRecord.objIndex and not QO.objIndex then QO.objIndex = learnerRecord.objIndex end
|
||||
end
|
||||
|
||||
-- A record that never captured a name -- a Learner entry written from an objective update, an
|
||||
-- override carrying a single key -- leaves QO.name nil, and this object is cached for the rest
|
||||
-- of the session, so everything downstream ends up printing the quest id. Ask the client.
|
||||
if ((not QO.name) or QO.name == "") and GetQuestLogIndexByID and GetQuestLogTitle then
|
||||
local questLogIndex = GetQuestLogIndexByID(questId)
|
||||
if questLogIndex and questLogIndex > 0 then
|
||||
local logTitle = GetQuestLogTitle(questLogIndex)
|
||||
if logTitle and logTitle ~= "" then
|
||||
QO.name = logTitle
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Same story for the zone: partial records leave it nil, and callers all treat it as a number
|
||||
-- (`quest.zoneOrSort > 0` errors outright on nil). 0 is the value everything already reads as
|
||||
-- "no zone on file", which sends the tracker to the quest log for one.
|
||||
QO.zoneOrSort = QO.zoneOrSort or 0
|
||||
|
||||
local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
|
||||
QO.level = questLevel
|
||||
QO.requiredLevel = requiredLevel
|
||||
|
||||
@@ -102,6 +102,20 @@ local trackerOptionsLocales = {
|
||||
["frFR"] = "Affiche le niveau des quêtes avec le titre des quêtes.",
|
||||
},
|
||||
---------------------------------------------------------
|
||||
["Show Objective Marker Button"] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
["When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them."] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
---------------------------------------------------------
|
||||
["Objective Marker Button Size"] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
["The size of the objective marker button shown next to each quest in the Questie Tracker."] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
---------------------------------------------------------
|
||||
["Auto Minimize Completed Quests"] = {
|
||||
["ptBR"] = "Minimizar missões concluídas",
|
||||
["ruRU"] = "Свернуть выполненные",
|
||||
@@ -864,6 +878,12 @@ local trackerOptionsLocales = {
|
||||
["esES"] = "Por zona",
|
||||
["frFR"] = "Par zone",
|
||||
},
|
||||
["By Zone + %% Complete"] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
["By Zone + %% Complete (Reversed)"] = {
|
||||
["enUS"] = true,
|
||||
},
|
||||
["By Zone Prox"] = {
|
||||
["ptBR"] = "Por proximidade de zona",
|
||||
["ruRU"] = "По дальности зоны",
|
||||
|
||||
@@ -47,13 +47,20 @@ function QuestieMap.utils:SetDrawOrder(frame)
|
||||
frame:SetFrameStrata(frameStrata)
|
||||
frame:SetFrameLevel(frameLevel)
|
||||
else
|
||||
-- If Magnify-WotLK is loaded, parent to WorldMapButton instead of WorldMapFrame
|
||||
-- If Magnify is loaded (standalone Magnify-WotLK, or bundled inside LootCollector,
|
||||
-- which has no separate .toc so IsAddOnLoaded("Magnify-WotLK") never sees it),
|
||||
-- parent to WorldMapButton and match ITS strata instead of WorldMapFrame's, so the
|
||||
-- icon stays inside Magnify's native WorldMapScrollFrame clip subtree (mirrors how
|
||||
-- LootCollector's own map pins are parented/strata'd in Modules/Map.lua)
|
||||
local magnifyLoaded = (IsAddOnLoaded and IsAddOnLoaded("Magnify-WotLK")) or (_G.LootCollectorMagnify ~= nil)
|
||||
local parent = WorldMapFrame
|
||||
if IsAddOnLoaded and IsAddOnLoaded("Magnify-WotLK") and _G.WorldMapButton then
|
||||
local strataSource = WorldMapFrame
|
||||
if magnifyLoaded and _G.WorldMapButton then
|
||||
parent = WorldMapButton
|
||||
strataSource = WorldMapButton
|
||||
end
|
||||
local frameLevel = WorldMapFrame:GetFrameLevel() + 7
|
||||
local frameStrata = WorldMapFrame:GetFrameStrata()
|
||||
local frameStrata = strataSource:GetFrameStrata()
|
||||
frame:SetParent(parent)
|
||||
frame:SetFrameStrata(frameStrata)
|
||||
frame:SetFrameLevel(frameLevel)
|
||||
|
||||
@@ -113,6 +113,8 @@ function QuestieOptionsDefaults:Load()
|
||||
autoTrackQuests = true,
|
||||
trackerShowCompleteQuests = true,
|
||||
trackerShowQuestLevel = true,
|
||||
trackerShowSuperTrackButton = true,
|
||||
trackerSuperTrackButtonSize = 25,
|
||||
collapseCompletedQuests = false,
|
||||
hideCompletedQuestObjectives = false,
|
||||
hideBlizzardCompletionText = false,
|
||||
|
||||
@@ -13,6 +13,8 @@ local TrackerBaseFrame = QuestieLoader:ImportModule("TrackerBaseFrame")
|
||||
local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool")
|
||||
---@type TrackerQuestTimers
|
||||
local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers")
|
||||
---@type TrackerUtils
|
||||
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
|
||||
---@type QuestieArrow
|
||||
local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow")
|
||||
|
||||
@@ -175,6 +177,37 @@ function QuestieOptions.tabs.tracker:Initialize()
|
||||
QuestieTracker:Update()
|
||||
end
|
||||
},
|
||||
showSuperTrackButton = {
|
||||
type = "toggle",
|
||||
order = 5,
|
||||
width = 1.5,
|
||||
name = function() return l10n('Show Objective Marker Button') end,
|
||||
desc = function() return l10n('When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them.') end,
|
||||
hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end,
|
||||
disabled = function() return not Questie.db.profile.trackerEnabled end,
|
||||
get = function() return Questie.db.profile.trackerShowSuperTrackButton end,
|
||||
set = function(_, value)
|
||||
Questie.db.profile.trackerShowSuperTrackButton = value
|
||||
QuestieTracker:Update()
|
||||
end
|
||||
},
|
||||
superTrackButtonSize = {
|
||||
type = "range",
|
||||
order = 6,
|
||||
width = 1.5,
|
||||
name = function() return l10n('Objective Marker Button Size') end,
|
||||
desc = function() return l10n('The size of the objective marker button shown next to each quest in the Questie Tracker.') end,
|
||||
hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end,
|
||||
disabled = function() return (not Questie.db.profile.trackerEnabled) or (not Questie.db.profile.trackerShowSuperTrackButton) end,
|
||||
min = 8,
|
||||
max = 70,
|
||||
step = 1,
|
||||
get = function() return Questie.db.profile.trackerSuperTrackButtonSize end,
|
||||
set = function(_, value)
|
||||
Questie.db.profile.trackerSuperTrackButtonSize = value
|
||||
QuestieTracker:Update()
|
||||
end
|
||||
},
|
||||
showQuestTimer = {
|
||||
type = "toggle",
|
||||
order = 3,
|
||||
@@ -402,6 +435,8 @@ function QuestieOptions.tabs.tracker:Initialize()
|
||||
['byProximity'] = l10n('By Proximity'),
|
||||
['byProximityReversed'] = l10n('By Proximity (Reversed)'),
|
||||
['byZone'] = l10n('By Zone'),
|
||||
['byZoneComplete'] = l10n('By Zone + %% Complete'),
|
||||
['byZoneCompleteReversed'] = l10n('By Zone + %% Complete (Reversed)'),
|
||||
['byZonePlayerProximity'] = l10n('By Zone Prox'),
|
||||
['byZonePlayerProximityReversed'] = l10n('By Zone Prox (Reversed)'),
|
||||
}
|
||||
|
||||
@@ -553,6 +553,10 @@ function _QuestEventHandler:QuestLogUpdate()
|
||||
doFullQuestLogScan = false
|
||||
-- Function call updates doFullQuestLogScan. Order matters.
|
||||
_QuestEventHandler:UpdateAllQuests()
|
||||
-- Also on this path: UpdateAllQuests only looks at quests still in the log, so a removal
|
||||
-- that fired no event of its own would sit there unnoticed for as long as full scans keep
|
||||
-- being asked for.
|
||||
_QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
else
|
||||
_QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
QuestieCombatQueue:Queue(function()
|
||||
@@ -666,7 +670,9 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
if QuestiePlayer and QuestiePlayer.currentQuestlog then
|
||||
local removedQuestIds = {}
|
||||
for questId in pairs(QuestiePlayer.currentQuestlog) do
|
||||
if questId and questId > 0 and (not gameQuestIds[questId]) then
|
||||
-- Typed check: a stray string key (saved variables have produced them) would other-
|
||||
-- wise error on the comparison and take the whole pass down with it.
|
||||
if type(questId) == "number" and questId > 0 and (not gameQuestIds[questId]) then
|
||||
removedQuestIds[#removedQuestIds + 1] = questId
|
||||
end
|
||||
end
|
||||
@@ -679,7 +685,12 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
|
||||
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
|
||||
local wasAlreadyComplete = Questie.db.char.complete and Questie.db.char.complete[questId]
|
||||
local completeAtRemoval = QuestieDB.IsComplete(questId)
|
||||
local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1
|
||||
-- The server's own record, and the only one that knows anything about a quest the
|
||||
-- database has never heard of: QuestieDB.IsComplete cannot answer for those, so an
|
||||
-- Ascension quest the server finished by itself would otherwise be filed as abandoned.
|
||||
local serverFlaggedComplete = IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId)
|
||||
local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1 or
|
||||
serverFlaggedComplete
|
||||
|
||||
QuestLogCache.RemoveQuest(questId)
|
||||
QuestieQuest:SetObjectivesDirty(questId)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
--- COMPATIBILITY ---
|
||||
local IsQuestFlaggedCompleted = QuestieCompat.IsQuestFlaggedCompleted or C_QuestLog.IsQuestFlaggedCompleted
|
||||
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
||||
|
||||
---@class QuestieQuest
|
||||
local QuestieQuest = QuestieLoader:CreateModule("QuestieQuest")
|
||||
@@ -2305,6 +2306,24 @@ function QuestieQuest:PopulateQuestLogInfo(quest)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sync level with the live quest log. QuestieDB.GetQuest permanently caches quest
|
||||
-- objects (Database/QuestieDB.lua:1752 `if _QuestieDB.questCache[questId] then return
|
||||
-- ... end`), so quest.level is otherwise frozen at whatever value was computed the
|
||||
-- first time this quest was cached. QuestLogCache.questLog_DO_NOT_MODIFY doesn't carry
|
||||
-- a level field at all, so read it straight from GetQuestLogTitle. On servers with
|
||||
-- dynamic quest level scaling (e.g. Ascension), the effective level can change after
|
||||
-- caching (player level-up, scaling option toggled), so re-sync it here every time.
|
||||
if GetQuestLogTitle then
|
||||
local questIndex = GetQuestLogIndexByID and GetQuestLogIndexByID(quest.Id)
|
||||
if questIndex and questIndex > 0 then
|
||||
local _, liveLevel = GetQuestLogTitle(questIndex)
|
||||
if liveLevel and liveLevel > 0 and liveLevel ~= quest.level then
|
||||
quest.level = liveLevel
|
||||
quest.questLevel = liveLevel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Live fallback quests (no static DB entry) manage their own Objectives.
|
||||
-- Their per-objective Update() functions read directly from QuestLogCache.
|
||||
if quest._isLogFallback then
|
||||
|
||||
@@ -46,6 +46,7 @@ local QuestieDebugOffer = QuestieLoader:ImportModule("QuestieDebugOffer")
|
||||
local C_Timer = QuestieCompat.C_Timer
|
||||
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
||||
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
|
||||
local GetQuestLogSpecialItemInfo = QuestieCompat.GetQuestLogSpecialItemInfo
|
||||
local GetItemInfo = QuestieCompat.GetItemInfo
|
||||
local function GetNumLines(label)
|
||||
if label.GetNumLines then
|
||||
@@ -62,6 +63,22 @@ local function GetWrappedWidth(label)
|
||||
return label:GetWidth()
|
||||
end
|
||||
|
||||
-- A quest built from partial data can reach the tracker with no name, or with the id standing in
|
||||
-- for one, and it is cached that way for the session. Ask the quest log before printing a number.
|
||||
local function GetDisplayableQuestName(quest)
|
||||
local questName = quest.name
|
||||
|
||||
if (not questName) or questName == "" or questName == tostring(quest.Id) then
|
||||
questName = TrackerUtils:GetQuestLogTitleById(quest.Id) or questName
|
||||
end
|
||||
|
||||
if (not questName) or questName == "" then
|
||||
questName = tostring(quest.Id)
|
||||
end
|
||||
|
||||
return questName
|
||||
end
|
||||
|
||||
local LSM30 = LibStub and LibStub("LibSharedMedia-3.0", true)
|
||||
|
||||
-- Local Vars
|
||||
@@ -334,6 +351,10 @@ function QuestieTracker.Initialize()
|
||||
Questie.db.profile.trackerSetpoint = "TOPLEFT"
|
||||
end
|
||||
|
||||
-- Tracks what the client considers supertracked. Permanent by design: hooksecurefunc cannot be
|
||||
-- undone, and the value has to stay correct even while the Questie tracker is disabled.
|
||||
TrackerUtils:InitSuperTrackHook()
|
||||
|
||||
if (not Questie.db.profile.trackerEnabled) then
|
||||
-- The Tracker is disabled, no need to continue
|
||||
return
|
||||
@@ -956,6 +977,10 @@ function QuestieTracker:Update()
|
||||
-- Safety check - make sure we didn't run over our linePool limit.
|
||||
if not line then return "BREAK" end
|
||||
|
||||
-- Kept so the supertrack button, which lives on the title line, can be
|
||||
-- re-centred over the finished quest block further down.
|
||||
local questTitleLine = line
|
||||
|
||||
-- Set Line Mode, Types, Clickers
|
||||
line:SetMode("quest")
|
||||
line:SetOnClick("quest")
|
||||
@@ -965,7 +990,7 @@ function QuestieTracker:Update()
|
||||
line.criteriaMark:Hide()
|
||||
|
||||
-- Set Min/Max Button and default states
|
||||
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
|
||||
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 4, 1)
|
||||
line.expandQuest.zoneId = zoneName
|
||||
|
||||
|
||||
@@ -1022,7 +1047,7 @@ function QuestieTracker:Update()
|
||||
|
||||
if quest.isFallback or quest._isLogFallback then
|
||||
-- Quest not in DB: use the name stored on the fallback object
|
||||
local questName = quest.name or tostring(quest.Id)
|
||||
local questName = GetDisplayableQuestName(quest)
|
||||
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
|
||||
questName = "[" .. quest.level .. "] " .. questName
|
||||
end
|
||||
@@ -1030,6 +1055,13 @@ function QuestieTracker:Update()
|
||||
questName = questName .. " (" .. quest.Id .. ")"
|
||||
end
|
||||
coloredQuestName = "|cFFFFFF00" .. questName .. "|r"
|
||||
-- Fallback quests (e.g. custom server quests not in QuestieDB) skip
|
||||
-- GetColoredQuestName entirely since it needs a DB name lookup, so they
|
||||
-- never got the (Complete) suffix DB quests get under the same setting.
|
||||
if Questie.db.profile.collapseCompletedQuests and isMinimizable then
|
||||
coloredQuestName = coloredQuestName .. " " ..
|
||||
Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
|
||||
end
|
||||
elseif timedQuest then
|
||||
coloredQuestName = QuestieLib:GetColoredQuestName(quest.Id,
|
||||
Questie.db.profile.trackerShowQuestLevel, false, false)
|
||||
@@ -1044,18 +1076,25 @@ function QuestieTracker:Update()
|
||||
-- server quest not yet in the static DB), so GetColoredQuestName
|
||||
-- returned nil -- fall back instead of SetText(nil) blanking the
|
||||
-- title line while its objectives still render normally below it.
|
||||
-- Level/tag lookups don't require a DB "name" entry, so reuse the
|
||||
-- same formatting helper as the normal path to keep the "[level]"
|
||||
-- prefix consistent with quests that do have a DB name.
|
||||
local fallbackName = quest.name or tostring(quest.Id)
|
||||
if Questie.db.profile.trackerShowQuestLevel then
|
||||
local level = QuestieLib.GetTbcLevel(quest.Id)
|
||||
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, level, false)
|
||||
-- Use the live quest object's own level (kept in sync by
|
||||
-- QuestieQuest:PopulateQuestLogInfo) instead of QuestieLib.GetTbcLevel,
|
||||
-- which queries the static DB only: for a quest with no DB entry it
|
||||
-- silently defaults to level 1, and Ascension's scaling then scales
|
||||
-- that wrong base instead of the quest's real level.
|
||||
local fallbackName = GetDisplayableQuestName(quest)
|
||||
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
|
||||
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false)
|
||||
end
|
||||
if Questie.db.profile.enableTooltipsQuestID then
|
||||
fallbackName = fallbackName .. " (" .. quest.Id .. ")"
|
||||
end
|
||||
coloredQuestName = "|cFFFFFF00" .. fallbackName .. "|r"
|
||||
-- Same suffix rule as the normal (DB) branch -- GetColoredQuestName
|
||||
-- never got a chance to run since it bailed out on the missing name.
|
||||
if Questie.db.profile.collapseCompletedQuests and isMinimizable then
|
||||
coloredQuestName = coloredQuestName .. " " ..
|
||||
Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
|
||||
end
|
||||
end
|
||||
|
||||
line.label:SetText(coloredQuestName)
|
||||
@@ -1078,8 +1117,32 @@ function QuestieTracker:Update()
|
||||
-- Adds the AI_VoiceOver Play Buttons
|
||||
line.playButton:SetPlayButton(questId)
|
||||
|
||||
-- Adds the button that points the floating objective marker at this quest.
|
||||
-- Must run after SetPlayButton, since it anchors around the play button.
|
||||
line.superTrackButton:SetSuperTrackButton(questId)
|
||||
|
||||
local usableQIB = false
|
||||
local sourceItemId = QuestieDB.QueryQuestSingle(quest.Id, "sourceItemId")
|
||||
local isLiveSourceItem = false
|
||||
|
||||
if not sourceItemId or sourceItemId == 0 then
|
||||
-- QuestieDB has no data for this quest (e.g. a custom server quest).
|
||||
-- Ask the client directly which item, if any, it considers this quest's
|
||||
-- usable "special item" instead of relying on static DB data that
|
||||
-- simply doesn't exist for quests Questie doesn't know about.
|
||||
local questLogIndex = GetQuestLogIndexByID and GetQuestLogIndexByID(quest.Id)
|
||||
if questLogIndex and questLogIndex > 0 and GetQuestLogSpecialItemInfo then
|
||||
local link = GetQuestLogSpecialItemInfo(questLogIndex)
|
||||
local liveItemId = link and tonumber(link:match("item:(%d+)"))
|
||||
if liveItemId then
|
||||
sourceItemId = liveItemId
|
||||
isLiveSourceItem = true
|
||||
quest.sourceItemId = liveItemId
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quest._liveSourceItem = isLiveSourceItem
|
||||
local sourceItem = sourceItemId and TrackerUtils:IsQuestItemUsable(sourceItemId)
|
||||
local requiredItems = quest.requiredSourceItems
|
||||
local requiredItem = requiredItems and TrackerUtils:IsQuestItemUsable(requiredItems[1])
|
||||
@@ -1132,7 +1195,8 @@ function QuestieTracker:Update()
|
||||
end
|
||||
|
||||
-- Attach button to Quest Title linePool
|
||||
button:SetPoint("TOPLEFT", button.line, "TOPLEFT", 0, 0)
|
||||
button:SetPoint("TOPLEFT", button.line, "TOPLEFT",
|
||||
TrackerLinePool.GetItemButtonOffset(), 0)
|
||||
button:SetParent(button.line)
|
||||
button:Show()
|
||||
|
||||
@@ -1146,6 +1210,10 @@ function QuestieTracker:Update()
|
||||
button:SetParent(UIParent)
|
||||
button:Hide()
|
||||
end
|
||||
|
||||
-- The quest item button owns this slot, so the marker steps out to
|
||||
-- the left of the line for as long as it is there.
|
||||
button.line.superTrackButton:SetItemButtonShown(button:IsShown())
|
||||
else
|
||||
-- Button failed to get setup for some reason or the quest item is now gone. Hide it and enable the Quest Min/Max button.
|
||||
-- See previous comment for details on why we're setting this button to UIParent.
|
||||
@@ -1240,7 +1308,7 @@ function QuestieTracker:Update()
|
||||
|
||||
-- Attach button to Quest Title linePool
|
||||
altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
|
||||
2 + questItemButtonSize, 0)
|
||||
TrackerLinePool.GetItemButtonOffset() + 2 + questItemButtonSize, 0)
|
||||
altButton:SetParent(altButton.line)
|
||||
altButton:Show()
|
||||
|
||||
@@ -1541,6 +1609,16 @@ function QuestieTracker:Update()
|
||||
|
||||
-- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options
|
||||
line:SetHeight(line.label:GetHeight() + (Questie.db.profile.trackerQuestPadding + 2))
|
||||
|
||||
-- Centre the supertrack button on the quest's whole text block now that its
|
||||
-- objective lines are drawn and their heights are final.
|
||||
if questTitleLine.superTrackButton.questId then
|
||||
local blockHeight = TrackerLinePool.GetQuestBlockHeight(questTitleLine, line)
|
||||
|
||||
if blockHeight then
|
||||
questTitleLine.superTrackButton:SetBlockHeight(blockHeight)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
primaryButton = false
|
||||
@@ -1678,7 +1756,7 @@ function QuestieTracker:Update()
|
||||
|
||||
-- Set Min/Max Button and default states
|
||||
line.expandQuest:Show()
|
||||
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
|
||||
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 4, 1)
|
||||
line.expandQuest.zoneId = zoneName
|
||||
|
||||
-- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown).
|
||||
|
||||
@@ -35,6 +35,67 @@ local l10n = QuestieLoader:ImportModule("l10n")
|
||||
local C_Timer = QuestieCompat.C_Timer
|
||||
local C_QuestLog = QuestieCompat.C_QuestLog
|
||||
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
|
||||
-- Copies one texture region of a quest pin onto our button. The texture file travels with the
|
||||
-- coordinates because the client swaps files between states, and the region is only shown when the
|
||||
-- pin itself shows it -- the pin uses "number" for in-progress quests and "turnin" for completed
|
||||
-- ones, never both.
|
||||
local function MirrorPinRegion(destination, source, size, pinSize)
|
||||
if (not source) or (not source:IsShown()) or (not source:GetTexture()) then
|
||||
destination:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- Regions are not all the size of the pin -- the "?" is drawn larger than a digit -- so scale
|
||||
-- them by how the pin itself was scaled instead of stretching each one to the button.
|
||||
local scale = (pinSize and pinSize > 0) and (size / pinSize) or 1
|
||||
local width = (source:GetWidth() or 0) * scale
|
||||
local height = (source:GetHeight() or 0) * scale
|
||||
if width <= 0 or height <= 0 then
|
||||
width = size
|
||||
height = size
|
||||
end
|
||||
|
||||
destination:SetWidth(width)
|
||||
destination:SetHeight(height)
|
||||
destination:SetTexture(source:GetTexture())
|
||||
destination:SetTexCoord(source:GetTexCoord())
|
||||
destination:Show()
|
||||
end
|
||||
|
||||
-- The pin atlases stack the selected (yellow circle, black digits) variant of every cell exactly
|
||||
-- half a texture above the normal one. The client only restyles its pins during the world map's own
|
||||
-- selection pass, which has not run yet right after a login or a reload, so the variant is forced
|
||||
-- here rather than taken on trust from the pin.
|
||||
local function ApplySelectedVariant(texture, selected)
|
||||
local topLeftX, topLeftY, bottomLeftX, bottomLeftY, topRightX, topRightY, bottomRightX, bottomRightY = texture:GetTexCoord()
|
||||
if (not topLeftY) or (not bottomLeftY) or (bottomLeftY - topLeftY) > 0.5 then
|
||||
return
|
||||
end
|
||||
|
||||
local offset
|
||||
if selected and topLeftY >= 0.5 then
|
||||
offset = -0.5
|
||||
elseif (not selected) and bottomLeftY <= 0.5 then
|
||||
offset = 0.5
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
texture:SetTexCoord(topLeftX, topLeftY + offset, bottomLeftX, bottomLeftY + offset,
|
||||
topRightX, topRightY + offset, bottomRightX, bottomRightY + offset)
|
||||
end
|
||||
|
||||
-- Whole-button states (hover, pressed) rather than the regions drawn inside them, so these fill the
|
||||
-- button on their own and only need the atlas cell copied across.
|
||||
local function MirrorPinButtonTexture(destination, source)
|
||||
if (not destination) or (not source) or (not source.GetTexture) or (not source:GetTexture()) then
|
||||
return
|
||||
end
|
||||
|
||||
destination:SetTexture(source:GetTexture())
|
||||
destination:SetTexCoord(source:GetTexCoord())
|
||||
end
|
||||
|
||||
local function GetNumLines(label)
|
||||
if label.GetNumLines then
|
||||
return label:GetNumLines()
|
||||
@@ -54,6 +115,22 @@ local linePool = {}
|
||||
local buttonPool = {}
|
||||
local lineMarginLeft = 10
|
||||
|
||||
-- Gap kept between the supertrack button and whatever it is tucked in next to.
|
||||
local superTrackButtonGap = 1
|
||||
|
||||
-- Left edge of a quest line's collapse button. QuestieTracker anchors it at questMarginLeft - 4
|
||||
-- with a width of trackerFontSizeQuest, and questMarginLeft carries a matching + trackerFontSizeQuest,
|
||||
-- so it lands on a flat 22 whatever the font size is.
|
||||
local superTrackCollapseButtonLeft = 22
|
||||
|
||||
-- Left edge of the quest item buttons. QuestieTracker anchors them there and the marker tucks in
|
||||
-- to the left of them, so the two have to agree on it.
|
||||
local questItemButtonLeft = 6
|
||||
|
||||
function TrackerLinePool.GetItemButtonOffset()
|
||||
return questItemButtonLeft
|
||||
end
|
||||
|
||||
---@param questFrame Frame
|
||||
function TrackerLinePool.Initialize(questFrame)
|
||||
local trackerQuestFrame = questFrame
|
||||
@@ -404,6 +481,197 @@ function TrackerLinePool.Initialize(questFrame)
|
||||
|
||||
line.playButton = playButton
|
||||
|
||||
-- create supertrack buttons for the Ascension floating objective marker
|
||||
local superTrackButton = CreateFrame("Button", "linePool.superTrackButton" .. i, line)
|
||||
superTrackButton:SetWidth(25)
|
||||
superTrackButton:SetHeight(25)
|
||||
superTrackButton:SetHitRectInsets(1, 1, 1, 1)
|
||||
|
||||
-- Hover and pressed states come from the pin atlas too, additively blended, exactly as the
|
||||
-- client's own pins do it. RefreshSuperTrackButton mirrors the pin's cells over these.
|
||||
superTrackButton:SetHighlightTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons", "ADD")
|
||||
superTrackButton:GetHighlightTexture():SetTexCoord(0.625, 0.75, 0.375, 0.5)
|
||||
superTrackButton:SetPushedTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons")
|
||||
|
||||
-- Same texture and atlas sub-rect the client's own quest POI pins use, so the tracker button
|
||||
-- reads as native rather than as an addon icon.
|
||||
superTrackButton:SetNormalTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons")
|
||||
superTrackButton:GetNormalTexture():SetTexCoord(0.5, 0.625, 0.875, 1)
|
||||
|
||||
-- The client marks the selected pin with this glow rather than by swapping the icon, so the
|
||||
-- tracker button highlights the same way the map pin does.
|
||||
superTrackButton.glow = superTrackButton:CreateTexture(nil, "BACKGROUND")
|
||||
superTrackButton.glow:SetTexture("Interface\\WorldMap\\UI-QuestPoi-IconGlow")
|
||||
superTrackButton.glow:SetBlendMode("ADD")
|
||||
superTrackButton.glow:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
|
||||
superTrackButton.glow:Hide()
|
||||
|
||||
-- The digit printed inside the pin is another cell of the same atlas, drawn over the icon.
|
||||
superTrackButton.number = superTrackButton:CreateTexture(nil, "OVERLAY")
|
||||
superTrackButton.number:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
|
||||
superTrackButton.number:Hide()
|
||||
|
||||
-- Completed quests draw a "?" from this separate region instead of a digit.
|
||||
superTrackButton.turnin = superTrackButton:CreateTexture(nil, "OVERLAY")
|
||||
superTrackButton.turnin:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
|
||||
superTrackButton.turnin:Hide()
|
||||
|
||||
-- The quest id is remembered even while the button is hidden. POI frames only exist once the
|
||||
-- world map has built them, so a quest that looks unreachable while the tracker is drawing
|
||||
-- can become reachable later -- without the id we would have nothing left to re-check.
|
||||
-- The pin's pressed state nudges what is drawn inside it down and to the right; the pushed
|
||||
-- texture covers the circle, this covers the digit and the "?".
|
||||
superTrackButton.SetPressedOffset = function(self, pressed)
|
||||
local offset = pressed and 1 or 0
|
||||
self.number:ClearAllPoints()
|
||||
self.number:SetPoint("CENTER", self, "CENTER", offset, -offset)
|
||||
self.turnin:ClearAllPoints()
|
||||
self.turnin:SetPoint("CENTER", self, "CENTER", offset, -offset)
|
||||
end
|
||||
|
||||
superTrackButton.SetSuperTrackButton = function(self, questId)
|
||||
self.questId = questId
|
||||
-- Back to the title line on its own: the objective lines below have not been laid out
|
||||
-- yet, so the tracker measures the block and calls SetBlockHeight once they are. Same
|
||||
-- for the quest item button, which is set up further down and owns this slot.
|
||||
self.blockHeight = nil
|
||||
self.itemButtonShown = nil
|
||||
self:RefreshSuperTrackButton()
|
||||
end
|
||||
|
||||
-- Horizontally: tucked in to the left of whatever else owns the head of the line -- the
|
||||
-- quest item button where there is one, the collapse button where there is not. It hangs
|
||||
-- over the line's left edge, and off the tracker entirely at larger sizes; giving the
|
||||
-- marker a column of its own would mean indenting every quest in the tracker for it.
|
||||
-- Vertically: centred on the quest's whole text block, title plus objectives, the height
|
||||
-- the tracker hands us. Without one, centred on the quest title alone, which is all that
|
||||
-- exists at the point the button is first set up.
|
||||
superTrackButton.AnchorSuperTrackButton = function(self)
|
||||
local buttonSize = Questie.db.profile.trackerSuperTrackButtonSize or 25
|
||||
local blockHeight = self.blockHeight or Questie.db.profile.trackerFontSizeQuest
|
||||
local slotLeft = self.itemButtonShown and questItemButtonLeft or superTrackCollapseButtonLeft
|
||||
local offsetX = slotLeft - superTrackButtonGap - buttonSize
|
||||
|
||||
-- The lines live inside the tracker's scroll frame, which clips anything hanging over
|
||||
-- its edge, so a marker that reaches past it is reparented above the clip. It stays
|
||||
-- anchored to its line either way, and every redraw hides it by hand
|
||||
-- (ResetLinesForChange) before deciding whether to show it again.
|
||||
self:SetParent(((offsetX + lineMarginLeft) < 0) and trackerQuestFrame or line)
|
||||
|
||||
self:ClearAllPoints()
|
||||
self:SetPoint("TOPLEFT", line, "TOPLEFT", offsetX, (buttonSize - blockHeight) / 2 + 1)
|
||||
|
||||
-- Has to sit above the tracker backdrop, which is what swallows a frame left at level 0.
|
||||
-- Strata comes from the line rather than from whichever parent it ended up with, so the
|
||||
-- two cases draw the same.
|
||||
self:SetFrameStrata(line:GetFrameStrata())
|
||||
self:SetFrameLevel(line:GetFrameLevel() + 10)
|
||||
end
|
||||
|
||||
superTrackButton.SetBlockHeight = function(self, blockHeight)
|
||||
if self.blockHeight == blockHeight then
|
||||
return
|
||||
end
|
||||
|
||||
self.blockHeight = blockHeight
|
||||
self:AnchorSuperTrackButton()
|
||||
end
|
||||
|
||||
superTrackButton.SetItemButtonShown = function(self, shown)
|
||||
shown = shown and true or false
|
||||
if (self.itemButtonShown or false) == shown then
|
||||
return
|
||||
end
|
||||
|
||||
self.itemButtonShown = shown
|
||||
self:AnchorSuperTrackButton()
|
||||
end
|
||||
|
||||
superTrackButton.RefreshSuperTrackButton = function(self)
|
||||
-- No map pin means the client has nothing to point the marker at (quest in another zone,
|
||||
-- or a quest without map coordinates), so there is nothing to offer.
|
||||
local pin = self.questId and TrackerUtils:GetSuperTrackPin(self.questId)
|
||||
if (not pin) or (not Questie.db.profile.trackerShowSuperTrackButton) then
|
||||
self:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local buttonSize = Questie.db.profile.trackerSuperTrackButtonSize or 25
|
||||
self:SetWidth(buttonSize)
|
||||
self:SetHeight(buttonSize)
|
||||
self.glow:SetWidth(buttonSize * 1.4)
|
||||
self.glow:SetHeight(buttonSize * 1.4)
|
||||
|
||||
-- Mirror the pin rather than picking atlas cells ourselves, so whatever the client
|
||||
-- decides to draw -- digit, "?", selected variant -- shows up here unchanged. The
|
||||
-- texture file has to be copied along with the coordinates: completed quests swap in a
|
||||
-- different file for these slots, and coordinates from one file applied to another
|
||||
-- sample nonsense.
|
||||
local isSuperTracked = TrackerUtils:GetSuperTrackedQuestId() == self.questId
|
||||
|
||||
local pinTexture = pin.GetNormalTexture and pin:GetNormalTexture()
|
||||
if pinTexture then
|
||||
local ownTexture = self:GetNormalTexture()
|
||||
ownTexture:SetTexture(pinTexture:GetTexture())
|
||||
ownTexture:SetTexCoord(pinTexture:GetTexCoord())
|
||||
ApplySelectedVariant(ownTexture, isSuperTracked)
|
||||
end
|
||||
|
||||
MirrorPinButtonTexture(self:GetHighlightTexture(), pin.GetHighlightTexture and pin:GetHighlightTexture())
|
||||
|
||||
local ownPushed = self:GetPushedTexture()
|
||||
MirrorPinButtonTexture(ownPushed, pin.GetPushedTexture and pin:GetPushedTexture())
|
||||
if ownPushed then
|
||||
ApplySelectedVariant(ownPushed, isSuperTracked)
|
||||
end
|
||||
|
||||
local pinSize = pin:GetWidth()
|
||||
MirrorPinRegion(self.number, pin.number, buttonSize, pinSize)
|
||||
MirrorPinRegion(self.turnin, pin.turnin, buttonSize, pinSize)
|
||||
if self.number:IsShown() then
|
||||
ApplySelectedVariant(self.number, isSuperTracked)
|
||||
end
|
||||
|
||||
-- Undo any leftover pressed offset: the pool recycles buttons, and a line can be redrawn
|
||||
-- while the mouse is still held down.
|
||||
self:SetPressedOffset(false)
|
||||
|
||||
self:AnchorSuperTrackButton()
|
||||
|
||||
-- The icon carries the selected variant itself; the glow is the one part the pin draws
|
||||
-- as a separate texture.
|
||||
if isSuperTracked then
|
||||
self.glow:Show()
|
||||
else
|
||||
self.glow:Hide()
|
||||
end
|
||||
|
||||
self:Show()
|
||||
end
|
||||
|
||||
superTrackButton:EnableMouse(true)
|
||||
superTrackButton:RegisterForClicks("LeftButtonUp")
|
||||
|
||||
superTrackButton:SetScript("OnMouseDown", function(self)
|
||||
self:SetPressedOffset(true)
|
||||
end)
|
||||
|
||||
superTrackButton:SetScript("OnMouseUp", function(self)
|
||||
self:SetPressedOffset(false)
|
||||
end)
|
||||
|
||||
superTrackButton:SetScript("OnClick", function(self)
|
||||
if self.questId then
|
||||
-- Same sound the client plays for its own quest pins.
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
TrackerUtils:SetSuperTrackedQuest(self.questId)
|
||||
end
|
||||
end)
|
||||
|
||||
superTrackButton:Hide()
|
||||
|
||||
line.superTrackButton = superTrackButton
|
||||
|
||||
-- create expanding buttons for quests with objectives
|
||||
local expandQuest = CreateFrame("Button", "linePool.expandQuest" .. i, line)
|
||||
expandQuest.texture = expandQuest:CreateTexture(nil, "OVERLAY", nil, 0)
|
||||
@@ -481,6 +749,9 @@ function TrackerLinePool.Initialize(questFrame)
|
||||
|
||||
line.expandQuest = expandQuest
|
||||
|
||||
-- Its own slot in the pool, so a run of lines drawn for one quest can be walked back over.
|
||||
line.lineIndex = i
|
||||
|
||||
linePool[i] = line
|
||||
nextFrame = line
|
||||
end
|
||||
@@ -504,7 +775,11 @@ function TrackerLinePool.Initialize(questFrame)
|
||||
|
||||
-- Check primary source item
|
||||
if quest.sourceItemId and quest.sourceItemId ~= 0 and buttonType == "primary" then
|
||||
if QuestieDB.QueryItemSingle(quest.sourceItemId, "class") == 12 and GetItemCount(quest.sourceItemId, false, false) > 0 then
|
||||
-- For quests QuestieDB has no data on, sourceItemId was resolved live via
|
||||
-- GetQuestLogSpecialItemInfo -- the client already vetted it as the quest's
|
||||
-- special item, so the static-DB item class isn't known/needed here.
|
||||
local isQuestItem = quest._liveSourceItem or QuestieDB.QueryItemSingle(quest.sourceItemId, "class") == 12
|
||||
if isQuestItem and GetItemCount(quest.sourceItemId, false, false) > 0 then
|
||||
foundItemId = quest.sourceItemId
|
||||
end
|
||||
end
|
||||
@@ -727,11 +1002,29 @@ function TrackerLinePool.ResetLinesForChange()
|
||||
line.playButton:SetAlpha(0)
|
||||
line.playButton:Hide()
|
||||
end
|
||||
if line.superTrackButton then
|
||||
line.superTrackButton.questId = nil
|
||||
line.superTrackButton:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
lineIndex = 0
|
||||
end
|
||||
|
||||
-- Re-evaluates the supertrack buttons without rebuilding the tracker. Driven by the
|
||||
-- SetSuperTrackedQuestID hook, which also fires on map open/close -- that is what makes buttons
|
||||
-- appear once the world map has built its POI frames, since the tracker itself does not redraw then.
|
||||
function TrackerLinePool.UpdateSuperTrackButtons()
|
||||
-- Rebuild the map's POI frames first so a zone change is picked up even with the map closed.
|
||||
TrackerUtils:PrimeSuperTrackFrames()
|
||||
|
||||
for _, line in pairs(linePool) do
|
||||
if line.superTrackButton and line.superTrackButton.questId then
|
||||
line.superTrackButton:RefreshSuperTrackButton()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TrackerLinePool.ResetButtonsForChange()
|
||||
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then
|
||||
Questie:Debug(Questie.DEBUG_SPAM, "[TrackerLinePool:ResetButtonsForChange]")
|
||||
@@ -818,6 +1111,49 @@ function TrackerLinePool.GetCurrentButton()
|
||||
return buttonPool[buttonIndex]
|
||||
end
|
||||
|
||||
-- Height of the block of lines a single quest was drawn into, title line through last objective.
|
||||
-- Added up from the heights the tracker itself set rather than measured off the frames: on login
|
||||
-- the tracker has not been laid out yet, and GetTop/GetBottom then report positions from a
|
||||
-- half-built frame, which is enough to fling a marker centred on the result off the tracker.
|
||||
---@return number|nil blockHeight
|
||||
function TrackerLinePool.GetQuestBlockHeight(firstLine, lastLine)
|
||||
if (not firstLine) or (not lastLine) or (not firstLine.lineIndex) or (not lastLine.lineIndex) then
|
||||
return nil
|
||||
end
|
||||
|
||||
if lastLine.lineIndex < firstLine.lineIndex then
|
||||
return nil
|
||||
end
|
||||
|
||||
local blockHeight = 0
|
||||
for i = firstLine.lineIndex, lastLine.lineIndex do
|
||||
local line = linePool[i]
|
||||
if not line then
|
||||
return nil
|
||||
end
|
||||
|
||||
blockHeight = blockHeight + line:GetHeight()
|
||||
end
|
||||
|
||||
-- The last line of a quest carries the padding to the next one. That is empty space below the
|
||||
-- text, so it plays no part in where the block's centre is. A quest collapsed down to its title
|
||||
-- is the exception: that single line is the whole quest, and neither reference reads right on
|
||||
-- its own -- against the text alone the marker rides high over the row's empty half, against
|
||||
-- the whole row it sits low under the title it belongs to -- so it splits the difference.
|
||||
local trailingPadding = Questie.db.profile.trackerQuestPadding + 2
|
||||
if lastLine.lineIndex == firstLine.lineIndex then
|
||||
trailingPadding = trailingPadding / 2
|
||||
end
|
||||
|
||||
blockHeight = blockHeight - trailingPadding
|
||||
|
||||
if blockHeight <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return blockHeight
|
||||
end
|
||||
|
||||
---@return table|nil lineIndex linePool[lineIndex - 1]
|
||||
function TrackerLinePool.GetPreviousLine()
|
||||
lineIndex = lineIndex - 1
|
||||
@@ -867,6 +1203,10 @@ function TrackerLinePool.HideUnusedLines()
|
||||
line.expandZone.zoneId = nil
|
||||
line.criteriaMark.mode = nil
|
||||
line.playButton.mode = nil
|
||||
-- Hidden by hand: a marker that overflows to the left of its line is parented above the
|
||||
-- scroll frame's clip, so hiding the line no longer hides it.
|
||||
line.superTrackButton.questId = nil
|
||||
line.superTrackButton:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1094,6 +1434,12 @@ end
|
||||
TrackerLinePool.SetMode = function(self, mode)
|
||||
if mode ~= self.mode then
|
||||
self.mode = mode
|
||||
-- Lines are recycled between zone headers, quest titles and objectives. Only quest title
|
||||
-- lines own a supertrack button, so drop it whenever a line takes on another role.
|
||||
if mode ~= "quest" and self.superTrackButton then
|
||||
self.superTrackButton.questId = nil
|
||||
self.superTrackButton:Hide()
|
||||
end
|
||||
if mode == "zone" then
|
||||
local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone
|
||||
self.label:SetFont((LSM30 and LSM30.Fetch and LSM30:Fetch("font", Questie.db.profile.trackerFontZone)) or Questie.db.profile.trackerFontZone, trackerFontSizeZone, Questie.db.profile.trackerFontOutline)
|
||||
|
||||
@@ -379,8 +379,14 @@ function TrackerUtils:GetCompletionText(quest)
|
||||
|
||||
if completionText then
|
||||
return completionText
|
||||
else
|
||||
elseif quest.Description and quest.Description[1] then
|
||||
return quest.Description[1]:gsub("%.", "")
|
||||
else
|
||||
-- Fallback/custom quests (e.g. server quests not in QuestieDB) have no
|
||||
-- top-level Description array. Without this, indexing quest.Description[1]
|
||||
-- throws and aborts the whole tracker render for that quest via the
|
||||
-- caller's pcall, freezing its line on the last successful render.
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -670,37 +676,162 @@ local function GetAreaIdByZoneName(zoneName)
|
||||
return l10n:GetAreaIdByLocalName(zoneName)
|
||||
end
|
||||
|
||||
-- Walk the quest log to find the zone header for a given questId.
|
||||
-- In 3.3.5, zone names appear as isHeader=true entries above their quests.
|
||||
-- Returns the header title string, or nil if not found.
|
||||
local function GetQuestLogZoneName(questId)
|
||||
local targetIndex = nil
|
||||
local total = GetNumQuestLogEntries and GetNumQuestLogEntries() or 0
|
||||
for i = 1, total do
|
||||
-- questId -> the header the client filed it under in the quest log. In 3.3.5 those headers are
|
||||
-- isHeader=true entries sitting above the quests they cover. Built in one pass and kept, because
|
||||
-- the tracker asks per quest and redraws often -- walking the log once per quest is quadratic.
|
||||
local questLogHeaders = {}
|
||||
|
||||
-- Every questId the log currently holds, headers aside. The tracker draws from currentQuestlog,
|
||||
-- and this is what says whether the player still has a given quest.
|
||||
local questLogQuestIds = {}
|
||||
|
||||
-- questId -> its row in the quest log, so re-reading a quest's leaderboard costs a lookup
|
||||
-- instead of another walk. Verified before use, since the log renumbers on every change.
|
||||
local questLogIndexes = {}
|
||||
|
||||
local function BuildQuestLogHeaders()
|
||||
local headers = {}
|
||||
local questIds = {}
|
||||
local indexes = {}
|
||||
local header
|
||||
|
||||
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
||||
local title, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
|
||||
if isHeader then
|
||||
if title and title ~= "" then
|
||||
header = title
|
||||
end
|
||||
elseif logId then
|
||||
questIds[logId] = true
|
||||
indexes[logId] = i
|
||||
if header then
|
||||
headers[logId] = header
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
questLogHeaders = headers
|
||||
questLogQuestIds = questIds
|
||||
questLogIndexes = indexes
|
||||
end
|
||||
|
||||
-- Returns the quest's current row in the quest log, or nil if the player no longer has it.
|
||||
local function GetQuestLogIndexForQuest(questId)
|
||||
local index = questLogIndexes[questId]
|
||||
if index then
|
||||
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(index)
|
||||
if (not isHeader) and logId == questId then
|
||||
return index
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
||||
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
|
||||
if not isHeader and logId == questId then
|
||||
targetIndex = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if not targetIndex then return nil end
|
||||
for i = targetIndex, 1, -1 do
|
||||
local title, _, _, isHeader = GetQuestLogTitle(i)
|
||||
if isHeader and title and title ~= "" then
|
||||
return title
|
||||
if (not isHeader) and logId == questId then
|
||||
questLogIndexes[questId] = i
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 3.3.5 hands over a leaderboard line as one string -- "Icefang slain: 3/8" -- while the tracker
|
||||
-- prints the description and the counts separately, so they have to come back apart here.
|
||||
-- Objectives with nothing to count arrive as bare text and stand in as a single 0/1 step.
|
||||
local function ParseLeaderBoardText(text, finished)
|
||||
-- Greedy on purpose: a description of its own may hold a colon, and the counter is the last one.
|
||||
local description, collected, needed = string.match(text, "^(.*):%s*(%d+)%s*/%s*(%d+)%s*$")
|
||||
if not description then
|
||||
-- Servers writing their own objective strings do not always keep the colon.
|
||||
description, collected, needed = string.match(text, "^(.-)%s*(%d+)%s*/%s*(%d+)%s*$")
|
||||
end
|
||||
|
||||
if (not description) or description == "" then
|
||||
return text, (finished and 1 or 0), 1
|
||||
end
|
||||
|
||||
collected = tonumber(collected) or 0
|
||||
needed = tonumber(needed) or 1
|
||||
if needed <= 0 then
|
||||
needed = 1
|
||||
end
|
||||
|
||||
return description, collected, needed
|
||||
end
|
||||
|
||||
-- Fills `objectives` from the quest's leaderboard, in place: the tracker hands these very tables
|
||||
-- to the lines it draws, so a redraw has to update them rather than swap in new ones.
|
||||
local function ReadFallbackObjectives(questLogIndex, questId, objectives)
|
||||
local numObjectives = (GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(questLogIndex)) or 0
|
||||
|
||||
for j = 1, numObjectives do
|
||||
local text, objectiveType, finished = GetQuestLogLeaderBoard(j, questLogIndex)
|
||||
if text then
|
||||
local description, collected, needed = ParseLeaderBoardText(text, finished)
|
||||
local objective = objectives[j]
|
||||
if not objective then
|
||||
objective = {}
|
||||
objectives[j] = objective
|
||||
end
|
||||
|
||||
objective.questId = questId
|
||||
objective.Index = j
|
||||
objective.Description = description
|
||||
objective.text = text
|
||||
objective.Collected = collected
|
||||
objective.Needed = needed
|
||||
objective.Completed = (finished or collected >= needed) and true or false
|
||||
objective.baseType = objectiveType
|
||||
-- Nothing here came from the DB, so map/tooltip code has to leave it alone.
|
||||
objective.Type = "fallback"
|
||||
end
|
||||
end
|
||||
|
||||
for j = #objectives, numObjectives + 1, -1 do
|
||||
objectives[j] = nil
|
||||
end
|
||||
|
||||
return objectives
|
||||
end
|
||||
|
||||
-- Returns the header title string, or nil if the quest is not in the log under one.
|
||||
local function GetQuestLogZoneName(questId)
|
||||
if not questLogQuestIds[questId] then
|
||||
-- Asked about a quest the last pass did not see, so the log has moved on since.
|
||||
BuildQuestLogHeaders()
|
||||
end
|
||||
|
||||
return questLogHeaders[questId]
|
||||
end
|
||||
|
||||
local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
||||
if zoneNameOverride and zoneNameOverride ~= "" then
|
||||
return zoneNameOverride
|
||||
end
|
||||
if not zoneOrSort then return "Unknown Zone" end
|
||||
|
||||
-- A quest assembled from a partial record -- a Learner entry that never captured the field,
|
||||
-- an override carrying a single key -- reaches here with no zoneOrSort at all. That is the
|
||||
-- same "nothing to look up" case as 0, and the quest log below still knows where the client
|
||||
-- files the quest, so it must not short-circuit to Unknown Zone ahead of that.
|
||||
zoneOrSort = zoneOrSort or 0
|
||||
|
||||
local zoneName
|
||||
local sortObj = Questie.db.profile.trackerSortObjectives
|
||||
if sortObj == "byZone" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
||||
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
||||
-- A server can file quests under categories of its own -- Ascension's "Ascension Main
|
||||
-- Quest" -- and those exist nowhere in the zone tables, so the quest data points at a zone
|
||||
-- instead: for anything the Learner recorded, whichever zone it was picked up in. A header
|
||||
-- that does not resolve to an area is one of those categories, and the client's own
|
||||
-- grouping is the only thing that knows about it.
|
||||
local logHeader = GetQuestLogZoneName(questId)
|
||||
if logHeader and logHeader ~= "" then
|
||||
local headerAreaId = GetAreaIdByZoneName(logHeader)
|
||||
if (not headerAreaId) or headerAreaId == 0 then
|
||||
return logHeader
|
||||
end
|
||||
end
|
||||
|
||||
if (zoneOrSort) > 0 then
|
||||
zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort)
|
||||
if not zoneName or zoneName == "Unknown Zone" then
|
||||
@@ -740,73 +871,119 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
||||
return zoneName
|
||||
end
|
||||
|
||||
-- The client's own title for a quest, or nil if the player does not have it. Quest objects can
|
||||
-- reach the tracker without a usable name -- a questDataOverrides entry that carries no name
|
||||
-- field, a QuestLogCache row read before the client had filled the title in -- and since those
|
||||
-- objects are cached for the session, the name never repairs itself. The log always knows.
|
||||
function TrackerUtils:GetQuestLogTitleById(questId)
|
||||
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
||||
if not questLogIndex then return nil end
|
||||
|
||||
local title = GetQuestLogTitle(questLogIndex)
|
||||
if title and title ~= "" then
|
||||
return title
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- IsComplete must be a method (called as quest:IsComplete()), and it reads the state the last
|
||||
-- refresh stored rather than closing over the state at build time -- these quests outlive many
|
||||
-- redraws, and a captured value would still claim the quest is unfinished after it is turned in.
|
||||
local function FallbackQuestIsComplete(self)
|
||||
if self.logIsComplete == 1 or self.logIsComplete == true then
|
||||
return 1
|
||||
end
|
||||
|
||||
if QuestiePlayer.currentQuestlog[self.Id] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(self.Id) then
|
||||
return 1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Re-reads everything the quest log owns: title, level, completion and the objectives. The
|
||||
-- tracker keeps fallback quests between draws, so without this they stay frozen at whatever
|
||||
-- the log said when the quest was first seen.
|
||||
---@return boolean @false if the player no longer has the quest
|
||||
function TrackerUtils:RefreshFallbackQuest(quest)
|
||||
if not quest then return false end
|
||||
|
||||
local questLogIndex = GetQuestLogIndexForQuest(quest.Id)
|
||||
if not questLogIndex then return false end
|
||||
|
||||
local title, level, _, _, _, isComplete = GetQuestLogTitle(questLogIndex)
|
||||
if title and title ~= "" then
|
||||
quest.name = title
|
||||
end
|
||||
if level and level > 0 then
|
||||
quest.level = level
|
||||
end
|
||||
|
||||
quest.logIsComplete = isComplete
|
||||
quest.isComplete = (isComplete == 1)
|
||||
|
||||
ReadFallbackObjectives(questLogIndex, quest.Id, quest.Objectives)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Returns nil if the quest is not currently in the quest log.
|
||||
function TrackerUtils:BuildFallbackQuest(questId)
|
||||
for i = 1, GetNumQuestLogEntries() do
|
||||
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i)
|
||||
if not isHeader and logQuestId == questId then
|
||||
-- Parse objectives from the leaderboard
|
||||
local objectives = {}
|
||||
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
|
||||
for j = 1, numObj do
|
||||
local text, _, finished = GetQuestLogLeaderBoard(j, i)
|
||||
if text then
|
||||
-- Parse "Description: X/Y" or just "Description"
|
||||
local collected, needed = string.match(text, ":.-(%d+)/(%d+)%s*$")
|
||||
collected = tonumber(collected) or (finished and 1 or 0)
|
||||
needed = tonumber(needed) or 1
|
||||
objectives[j] = {
|
||||
text = text,
|
||||
Needed = needed,
|
||||
Collected = collected,
|
||||
Finished = finished or (collected >= needed),
|
||||
Type = "fallback",
|
||||
}
|
||||
end
|
||||
end
|
||||
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
||||
if not questLogIndex then return nil end
|
||||
|
||||
-- Walk backwards from i in the quest log to find the zone header.
|
||||
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
||||
local zoneText = nil
|
||||
for h = i, 1, -1 do
|
||||
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
||||
if hIsHeader and hTitle and hTitle ~= "" then
|
||||
zoneText = hTitle
|
||||
break
|
||||
end
|
||||
end
|
||||
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
||||
local zoneNameOverride = nil
|
||||
if zoneText and (not zoneId or zoneId == 0) then
|
||||
zoneNameOverride = zoneText
|
||||
end
|
||||
|
||||
local quest = {
|
||||
Id = questId,
|
||||
name = title or ("Quest " .. questId),
|
||||
level = level or 0,
|
||||
zoneOrSort = zoneId,
|
||||
zoneName = zoneText,
|
||||
zoneNameOverride = zoneNameOverride,
|
||||
Objectives = objectives,
|
||||
SpecialObjectives = {},
|
||||
isFallback = true,
|
||||
}
|
||||
-- IsComplete must be a method (called as quest:IsComplete())
|
||||
quest.IsComplete = function(self)
|
||||
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0
|
||||
end
|
||||
|
||||
return quest
|
||||
-- Walk backwards from the quest in the log to find the zone header.
|
||||
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
||||
local zoneText = nil
|
||||
for h = questLogIndex, 1, -1 do
|
||||
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
||||
if hIsHeader and hTitle and hTitle ~= "" then
|
||||
zoneText = hTitle
|
||||
break
|
||||
end
|
||||
end
|
||||
return nil
|
||||
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
||||
local zoneNameOverride = nil
|
||||
if zoneText and (not zoneId or zoneId == 0) then
|
||||
zoneNameOverride = zoneText
|
||||
end
|
||||
|
||||
local quest = {
|
||||
Id = questId,
|
||||
name = "Quest " .. questId,
|
||||
level = 0,
|
||||
zoneOrSort = zoneId,
|
||||
zoneName = zoneText,
|
||||
zoneNameOverride = zoneNameOverride,
|
||||
Objectives = {},
|
||||
SpecialObjectives = {},
|
||||
isFallback = true,
|
||||
IsComplete = FallbackQuestIsComplete,
|
||||
}
|
||||
|
||||
-- Same read the tracker does on every later draw, so a quest built here and one refreshed
|
||||
-- from the cache carry exactly the same fields.
|
||||
TrackerUtils:RefreshFallbackQuest(quest)
|
||||
|
||||
return quest
|
||||
end
|
||||
|
||||
function TrackerUtils:GetSortedQuestIds()
|
||||
local sortedQuestIds = {}
|
||||
local questDetails = {}
|
||||
local sortObj = Questie.db.profile.trackerSortObjectives
|
||||
|
||||
-- One walk of the quest log for the whole draw, so the per-quest lookups below are reads.
|
||||
BuildQuestLogHeaders()
|
||||
|
||||
-- currentQuestlog is only as good as the removal events that maintain it, and a quest the
|
||||
-- server finishes on its own -- Ascension's auto-complete quests -- can leave the log without
|
||||
-- any of them landing, which strands the quest in the tracker for the rest of the session. The
|
||||
-- log is the authority on what the player still has, so anything missing from it is skipped.
|
||||
-- Skipped rather than pruned: a redraw that catches the log mid-refresh would otherwise throw
|
||||
-- away state Questie is about to want back.
|
||||
local questLogIsReadable = next(questLogQuestIds) ~= nil
|
||||
-- Update quest objectives
|
||||
|
||||
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
|
||||
@@ -872,6 +1049,11 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
if fallback then
|
||||
TrackerUtils._fallbackQuests[qid] = fallback
|
||||
end
|
||||
else
|
||||
-- Cached by an earlier draw, so its objectives and completion state are as
|
||||
-- old as the cache. Nothing else updates them -- these quests have no DB
|
||||
-- entry, so QuestieQuest's populate path skips them entirely.
|
||||
TrackerUtils:RefreshFallbackQuest(fallback)
|
||||
end
|
||||
if fallback then
|
||||
quest = fallback
|
||||
@@ -879,7 +1061,14 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
end
|
||||
end
|
||||
|
||||
if type(quest) == "table" and quest.IsComplete and quest.Objectives then
|
||||
local isInQuestLog = (not questLogIsReadable) or (questLogQuestIds[qid] == true)
|
||||
if not isInQuestLog then
|
||||
-- Left over from a removal nothing told the tracker about, so the object built for it
|
||||
-- goes too -- otherwise it would still be here to serve the next draw.
|
||||
TrackerUtils._fallbackQuests[qid] = nil
|
||||
end
|
||||
|
||||
if isInQuestLog and type(quest) == "table" and quest.IsComplete and quest.Objectives then
|
||||
-- Insert Quest Ids into sortedQuestIds table
|
||||
tinsert(sortedQuestIds, qid)
|
||||
|
||||
@@ -958,6 +1147,34 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
end
|
||||
end
|
||||
end)
|
||||
elseif sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" then
|
||||
table.sort(sortedQuestIds, function(a, b)
|
||||
local qAZone = questDetails[a].zoneName
|
||||
local qBZone = questDetails[b].zoneName
|
||||
|
||||
-- Sort by Zone first, then by % Complete within each zone
|
||||
-- (byZoneComplete: completes at top, 0% at bottom; reversed: opposite)
|
||||
if qAZone == qBZone then
|
||||
local vA, vB = questDetails[a].questCompletePercent, questDetails[b].questCompletePercent
|
||||
if vA == vB then
|
||||
local qA = questDetails[a].quest
|
||||
local qB = questDetails[b].quest
|
||||
return qA and qB and qA.level < qB.level
|
||||
end
|
||||
|
||||
if sortObj == "byZoneComplete" then
|
||||
return vA > vB
|
||||
else
|
||||
return vA < vB
|
||||
end
|
||||
else
|
||||
if qAZone ~= nil and qBZone ~= nil then
|
||||
return qAZone < qBZone
|
||||
else
|
||||
return qAZone and qBZone
|
||||
end
|
||||
end
|
||||
end)
|
||||
elseif sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
||||
local toSort = {}
|
||||
local continent = _GetContinent(C_Map.GetBestMapForUnit("player"))
|
||||
@@ -1231,6 +1448,273 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
return sortedQuestIds, questDetails
|
||||
end
|
||||
|
||||
-- Ascension's 3.3.5 client backports the retail floating objective marker ("SuperTracker").
|
||||
-- Supertracking is a slave of the quest log selection: the world map and the Blizzard watch frame
|
||||
-- both funnel through SelectQuestLogEntry -> SuperTrackerUtil.SetToBestSuperTrackingType. Calling
|
||||
-- C_SuperTrack.SetSuperTrackedQuestID directly only moves the marker until the next map interaction
|
||||
-- stomps it, so we click the same POI frames the client itself clicks.
|
||||
local superTrackedQuestId
|
||||
local superTrackHooked
|
||||
local superTrackRefreshing
|
||||
local superTrackRefreshPending
|
||||
local superTrackEventFrame
|
||||
|
||||
-- Refreshing rebuilds the map's POI frames, which can land back in the very hooks that asked for the
|
||||
-- refresh, so this is the only way the buttons are ever repainted. Requests are also coalesced:
|
||||
-- callers like TrackerQuestTimers select a quest log entry and immediately restore the previous one,
|
||||
-- so reading the selection on the first of those calls would catch a state the client is about to
|
||||
-- undo. Waiting a tick means the burst has settled.
|
||||
local function RefreshSuperTrackButtons()
|
||||
if superTrackRefreshing or superTrackRefreshPending then
|
||||
return
|
||||
end
|
||||
|
||||
superTrackRefreshPending = true
|
||||
C_Timer.After(0.05, function()
|
||||
superTrackRefreshPending = false
|
||||
superTrackRefreshing = true
|
||||
TrackerLinePool.UpdateSuperTrackButtons()
|
||||
superTrackRefreshing = false
|
||||
end)
|
||||
end
|
||||
|
||||
function TrackerUtils:IsSuperTrackAvailable()
|
||||
return (C_SuperTrack ~= nil) and ((WorldMapFrame_SelectQuestFrame ~= nil) or (WatchFrameQuestPOI_OnClick ~= nil))
|
||||
end
|
||||
|
||||
-- Most paths that change the supertracked quest end up in SetSuperTrackedQuestID -- our own button,
|
||||
-- world map pins, the map quest list, the Blizzard tracker, and the automatic re-pick that happens
|
||||
-- when the map switches zone -- so hooking it stands in for the GetSuperTrackedQuestID getter this
|
||||
-- client dropped. It goes quiet while the player is a ghost, though: the corpse arrow takes the
|
||||
-- marker over, so no quest is ever handed to it even though the map keeps selecting one. The quest
|
||||
-- selection itself is therefore hooked as well, and that is what keeps the buttons honest while
|
||||
-- dead. Caching what we last set would go stale the moment the player changed it by other means.
|
||||
function TrackerUtils:InitSuperTrackHook()
|
||||
if superTrackHooked or (not C_SuperTrack) then
|
||||
return
|
||||
end
|
||||
|
||||
superTrackHooked = true
|
||||
|
||||
hooksecurefunc(C_SuperTrack, "SetSuperTrackedQuestID", function(questId)
|
||||
superTrackedQuestId = questId
|
||||
RefreshSuperTrackButtons()
|
||||
end)
|
||||
|
||||
if C_SuperTrack.ClearSuperTracker then
|
||||
hooksecurefunc(C_SuperTrack, "ClearSuperTracker", function()
|
||||
superTrackedQuestId = nil
|
||||
RefreshSuperTrackButtons()
|
||||
end)
|
||||
end
|
||||
|
||||
if WorldMapFrame_SelectQuestFrame then
|
||||
hooksecurefunc("WorldMapFrame_SelectQuestFrame", RefreshSuperTrackButtons)
|
||||
end
|
||||
|
||||
if WatchFrameQuestPOI_OnClick then
|
||||
hooksecurefunc("WatchFrameQuestPOI_OnClick", RefreshSuperTrackButtons)
|
||||
end
|
||||
|
||||
-- Everything that supertracks a quest goes through the quest log selection, map or no map, alive
|
||||
-- or dead -- including opening a quest in the quest log window, which no other hook here sees.
|
||||
if SelectQuestLogEntry then
|
||||
hooksecurefunc("SelectQuestLogEntry", RefreshSuperTrackButtons)
|
||||
end
|
||||
|
||||
-- Nothing at all fires on a login or a reload: the map has not been touched, so the hooks above
|
||||
-- stay silent and the tracker draws before the client has styled its pins. These events are the
|
||||
-- only prompt to go back and look.
|
||||
superTrackEventFrame = CreateFrame("Frame")
|
||||
superTrackEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
superTrackEventFrame:RegisterEvent("PLAYER_UNGHOST")
|
||||
superTrackEventFrame:RegisterEvent("PLAYER_ALIVE")
|
||||
superTrackEventFrame:RegisterEvent("PLAYER_DEAD")
|
||||
superTrackEventFrame:SetScript("OnEvent", function()
|
||||
RefreshSuperTrackButtons()
|
||||
-- The client fills in its POI frames a moment after entering the world, so the immediate
|
||||
-- pass above can still come up empty.
|
||||
C_Timer.After(2, RefreshSuperTrackButtons)
|
||||
end)
|
||||
end
|
||||
|
||||
-- The hook only hears about the changes the client itself makes, and while the player is a ghost the
|
||||
-- floating marker is disabled: selecting a quest then never reaches SetSuperTrackedQuestID, so the
|
||||
-- hook reports nothing after a login or a reload in that state and goes stale after any click. The
|
||||
-- client's own frames still know. Watch frame POI buttons say so outright, and the world map's quest
|
||||
-- frames say it through their art: the atlases stack the selected (yellow) variant of a cell half a
|
||||
-- texture above the normal one, so a pin drawn from the upper half is the supertracked one.
|
||||
local function FindSelectedQuestId()
|
||||
for i = 1, 30 do
|
||||
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
|
||||
if not firstInRow then
|
||||
break
|
||||
end
|
||||
for j = 1, 5 do
|
||||
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
|
||||
if not poiButton then
|
||||
break
|
||||
end
|
||||
if poiButton.isSelected and poiButton.questId then
|
||||
return poiButton.questId
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, 25 do
|
||||
local questFrame = _G["WorldMapQuestFrame" .. i]
|
||||
if not questFrame then
|
||||
break
|
||||
end
|
||||
|
||||
local pin = questFrame.ownPOI or questFrame.poiIcon
|
||||
local pinTexture = pin and pin.GetNormalTexture and pin:GetNormalTexture()
|
||||
if pinTexture and questFrame.questId then
|
||||
local _, topY = pinTexture:GetTexCoord()
|
||||
if topY and topY < 0.5 then
|
||||
return questFrame.questId
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Right after a login or a reload no pin is styled at all: the client marks them the first time
|
||||
-- the world map is opened. The quest log selection is what it reads when it gets there, so it
|
||||
-- answers for the gap in between.
|
||||
local selection = GetQuestLogSelection and GetQuestLogSelection()
|
||||
if selection and selection > 0 then
|
||||
-- GetQuestIDFromLogIndex, not the raw API: GetQuestLogTitle is the compat wrapper here,
|
||||
-- which normalises the client's 9 return values down to 8.
|
||||
local questId = QuestieCompat.GetQuestIDFromLogIndex(selection)
|
||||
if questId and questId ~= 0 then
|
||||
return questId
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- The client is asked before the hook: the hook cannot see a ghost's selection changes at all, so
|
||||
-- its value is the fallback for when no POI frame exists to read (another zone, or frames not built
|
||||
-- yet), not the source of truth.
|
||||
function TrackerUtils:GetSuperTrackedQuestId()
|
||||
return FindSelectedQuestId() or superTrackedQuestId
|
||||
end
|
||||
|
||||
-- The world map builds its quest POI frames lazily, so right after login -- or after a zone change
|
||||
-- with the map still closed -- there is nothing to match a quest against and every button would
|
||||
-- hide itself. The client can build them without the map being shown, and doing so does not disturb
|
||||
-- which quest is currently supertracked. Skipped while the map is open so we never fight the player.
|
||||
function TrackerUtils:PrimeSuperTrackFrames()
|
||||
if WorldMapFrame and WorldMapFrame:IsShown() then
|
||||
return
|
||||
end
|
||||
|
||||
if WorldMapFrame_UpdateQuests then
|
||||
WorldMapFrame_UpdateQuests()
|
||||
end
|
||||
end
|
||||
|
||||
---@return table|nil frame, function|nil clickHandler
|
||||
local function GetSuperTrackFrame(questId)
|
||||
if (not questId) or questId == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Blizzard watch frame POI buttons carry the quest id directly. Questie empties the Blizzard
|
||||
-- watch list (see QuestieTracker:AQW_Insert) so only a handful of quests ever have one. Both
|
||||
-- scans below stop at the first gap: these frames are created in order, so a missing index
|
||||
-- means there are no further ones and this runs once per quest line per redraw.
|
||||
if WatchFrameQuestPOI_OnClick then
|
||||
for i = 1, 30 do
|
||||
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
|
||||
if not firstInRow then
|
||||
break
|
||||
end
|
||||
for j = 1, 5 do
|
||||
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
|
||||
if not poiButton then
|
||||
break
|
||||
end
|
||||
if poiButton.questId == questId then
|
||||
return poiButton, WatchFrameQuestPOI_OnClick
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- World map quest frames cover every quest with a POI on the currently viewed map, which is the
|
||||
-- wider net of the two. Pass the WorldMapQuestFrame itself and never its poiIcon --
|
||||
-- WorldMapFrame_SelectQuestFrame indexes questFrame.poiIcon and errors on the POI frame.
|
||||
if WorldMapFrame_SelectQuestFrame then
|
||||
if not _G["WorldMapQuestFrame1"] then
|
||||
TrackerUtils:PrimeSuperTrackFrames()
|
||||
end
|
||||
|
||||
for i = 1, 25 do
|
||||
local questFrame = _G["WorldMapQuestFrame" .. i]
|
||||
if not questFrame then
|
||||
break
|
||||
end
|
||||
if questFrame.questId == questId then
|
||||
return questFrame, WorldMapFrame_SelectQuestFrame
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- A quest can only be supertracked while it has a POI frame, so quests in another zone or without
|
||||
-- map coordinates simply have no button.
|
||||
function TrackerUtils:CanSuperTrackQuest(questId)
|
||||
return GetSuperTrackFrame(questId) ~= nil
|
||||
end
|
||||
|
||||
-- The map pin the client draws for this quest. Copying its texture coordinates is what keeps the
|
||||
-- tracker button identical to the pin: the number, the "?" shown for completed quests and the
|
||||
-- black-on-yellow selected variant all follow along without us mapping atlas cells by hand. The
|
||||
-- number is also the pin's position among quests that actually have a POI, which is not the same as
|
||||
-- the quest frame index -- another reason to read it from the client instead of deriving it.
|
||||
---@return table|nil
|
||||
function TrackerUtils:GetSuperTrackPin(questId)
|
||||
if (not questId) or questId == 0 or (not WorldMapFrame_SelectQuestFrame) then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not _G["WorldMapQuestFrame1"] then
|
||||
TrackerUtils:PrimeSuperTrackFrames()
|
||||
end
|
||||
|
||||
for i = 1, 25 do
|
||||
local questFrame = _G["WorldMapQuestFrame" .. i]
|
||||
if not questFrame then
|
||||
break
|
||||
end
|
||||
if questFrame.questId == questId then
|
||||
-- ownPOI is the pin drawn in the map's quest list, poiIcon the one on the map itself.
|
||||
-- The list version is the one we mirror: it keeps the circular background on completed
|
||||
-- quests, where the map version draws a bare "?".
|
||||
return questFrame.ownPOI or questFrame.poiIcon
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function TrackerUtils:SetSuperTrackedQuest(questId)
|
||||
local frame, clickHandler = GetSuperTrackFrame(questId)
|
||||
if not frame then
|
||||
return false
|
||||
end
|
||||
|
||||
clickHandler(frame)
|
||||
|
||||
-- Refresh here rather than leaning on the SetSuperTrackedQuestID hook: it stays silent while the
|
||||
-- player is a ghost, which would leave the button we just clicked looking untouched.
|
||||
RefreshSuperTrackButtons()
|
||||
return true
|
||||
end
|
||||
|
||||
function TrackerUtils:IsVoiceOverLoaded()
|
||||
-- Require not just that the VoiceOver addons are loaded, but that the runtime
|
||||
-- structure we index actually exists. Some VoiceOver builds (e.g. on Elune) expose
|
||||
|
||||
@@ -1,408 +1,26 @@
|
||||
> **Notice:** This is a maintenance release, not the completed performance refactor. The performance refactor is still in progress on the `phase2-lua50-sweep` branch. This release (v1.6.4) contains bug fixes and improvements over v1.6.3 and should be more stable, but the full performance work will land in a future release. If you are currently on v1.6.3, this release is a recommended upgrade.
|
||||
# Questie-X
|
||||
|
||||
> Current refactor focus: reducing QuestieLearner pin redraws during heavy kill activity, adding live Advanced performance controls for QuestieLearner and QuestieComms, adding Arrow throttles for low-end systems, and suppressing non-fatal error spam outside Questie debug modes. These changes are being staged on feature branches first and still need in-game validation before they become the next stable release.
|
||||
Fork of [aron-w/Questie-X](https://github.com/aron-w/Questie-X) with bug fixes and personal changes.
|
||||
Made for and tested on CoA, but it should also work on other 3.3.5 clients.
|
||||
|
||||
<div align="center">
|
||||
## Fixes
|
||||
|
||||
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
|
||||
- fixed world map icons offset due to Ascension's objective frame and zoom addons like Magnify.
|
||||
- fixed tracker objectives not updating correctly.
|
||||
- fixed tracker handle not moving the frame.
|
||||
- fixed custom quests having no name in the tracker.
|
||||
- fixed custom quests not showing (complete) in the tracker.
|
||||
- fixed custom quest items not showing.
|
||||
- fixed quest tags (group, elite, dungeon...) being wrong on quest ids Ascension reuses for its own content.
|
||||
- fixed Ascension "main quests" not having the correct category and not clearing on complete.
|
||||
|
||||

|
||||
[](https://github.com/Xurkon/Questie-X/releases)
|
||||
[](https://xurkon.github.io/Questie-X/)
|
||||
[](https://www.patreon.com/Xurkon)
|
||||
[](https://www.paypal.me/Xurkon)
|
||||

|
||||
## Changes
|
||||
|
||||
<br/>
|
||||
|
||||
**A universal WoW quest-helper with a plugin architecture for any private server.**
|
||||
|
||||
[Download Latest](https://github.com/Xurkon/Questie-X/releases/latest) • [View Source](https://github.com/Xurkon/Questie-X) • [Documentation](https://xurkon.github.io/Questie-X/)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
Questie-X is a fork of the original [Questie](https://github.com/Questie/Questie) addon, rebuilt to run reliably on any private server regardless of realm type or custom content. It fixes longstanding Lua errors, corrects API incompatibilities introduced by custom server emulators, and introduces a plugin system so server-specific quest databases can be distributed and maintained separately from the core addon.
|
||||
|
||||
---
|
||||
|
||||
## Current Performance Refactor Status
|
||||
|
||||
The active performance work is split across review branches so each phase can be reverted independently if needed.
|
||||
|
||||
| Area | Current Status |
|
||||
|------|----------------|
|
||||
| QuestieLearner | Kill-triggered map-pin refreshes are now debounced with a maximum wait cap, bystander `UNIT_DIED` events no longer force learner pin redraws, and `PARTY_KILL` events are protected from being suppressed by earlier `UNIT_DIED` debounce entries. |
|
||||
| QuestieComms | Advanced options include a full comms disable switch plus throttles for processing, quest-state broadcasts, and bulk sync behavior. |
|
||||
| Arrow | Advanced options include update throttles that apply live and are intended to reduce repeated target/coordinate work on lower-end PCs. |
|
||||
| Error noise | Missing quest and non-fatal database messages are being routed to Questie debug-critical/developer output instead of normal chat spam. |
|
||||
| Phase 3 hot paths | A measured branch contains additional localization, tooltip/map, quest eligibility, and cache-allocation improvements that still need integration with the learner/comms branch. |
|
||||
|
||||
Before the next stable release, the combined branch needs in-game testing in heavy kill zones with the minimap open, nearby players killing mobs, comms disabled/low/normal/fast, and Arrow throttles adjusted live from the options menu.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
> **⚠️ Two addons are always required — even on supported servers**
|
||||
>
|
||||
> Questie-X **cannot run alone**. It has no quest, NPC, item, or object data bundled inside it — that data lives entirely in a separate database plugin. You must install **both** the core engine **and** a server-specific database plugin for Questie-X to display anything.
|
||||
>
|
||||
> **Which database plugin you need depends on your server** — see the table in Step 2 below.
|
||||
|
||||
> **⚠️ Upgrading from the old architecture (Questie-335 / PE-Questie / any pre-v1.1.4 Questie fork)?**
|
||||
> The old addon loaded its database from files inside the core folder. Questie-X loads data from a separate plugin addon. These two systems are **not compatible** — you must fully remove the old installation before installing Questie-X, or you will get conflicts, duplicate modules, and data errors.
|
||||
>
|
||||
> **Before you install:**
|
||||
> 1. Open `Interface/AddOns/` and **delete** any folder named `Questie`, `Questie-335`, `PE-Questie`, `Questie-X` (if updating), or any other Questie variant.
|
||||
> 2. Delete any associated saved-variable files: `WTF/Account/<name>/SavedVariables/Questie*.lua`.
|
||||
> 3. Then follow the fresh install steps below.
|
||||
|
||||
### Step 1 — Install Questie-X Core
|
||||
|
||||
1. [Download](https://github.com/Xurkon/Questie-X/releases/latest) the latest `Questie-X` release `.zip`.
|
||||
2. Extract the archive — you will get a folder named `Questie-X`.
|
||||
3. Move that folder into your `Interface/AddOns/` directory:
|
||||
```
|
||||
World of Warcraft/
|
||||
└── Interface/
|
||||
└── AddOns/
|
||||
└── Questie-X/ ← place it here
|
||||
```
|
||||
|
||||
### Step 2 — Install Your Server's Database Plugin
|
||||
|
||||
> **⚠️ This step is not optional. Questie-X will not work without a database plugin loaded.**
|
||||
|
||||
Questie-X **requires** a database plugin to function. The plugin provides quest, NPC, object, and item data specific to your private server. Without it, Questie-X has no data to display.
|
||||
|
||||
**Two categories of servers:**
|
||||
|
||||
| Your Server Type | What to Install |
|
||||
|-----------------|----------------|
|
||||
| **Supported server (see table below)** | Download that server's plugin |
|
||||
| **Custom/unlisted server** | Use QuestieLearner to crowdsource data; see Note at bottom of this section |
|
||||
|
||||
**Download and install the plugin for your server from the table below:**
|
||||
|
||||
> **💡 All server plugins also require Questie-X-WotLKDB as the base layer.** Your server-specific plugin (AscensionDB, ClassicDB, etc.) provides the quest data overrides and custom content — but Questie-X-WotLKDB is still needed underneath it for the core WotLK quest data that your server's plugin extends. Install both.
|
||||
|
||||
| Your Server | Plugin to Download | Repository |
|
||||
|-------------|------------------|------------|
|
||||
| WotLK 3.3.5 (most private servers) | **Questie-X-WotLKDB** | [Xurkon/Questie-X-WotLKDB](https://github.com/Xurkon/Questie-X-WotLKDB) |
|
||||
| Classic Era / Vanilla 1.14.x | **Questie-X-ClassicDB** | [Xurkon/Questie-X-ClassicDB](https://github.com/Xurkon/Questie-X-ClassicDB) |
|
||||
| TBC 2.5.x | **Questie-X-TBCDB** | [Xurkon/Questie-X-TBCDB](https://github.com/Xurkon/Questie-X-TBCDB) |
|
||||
| Project Ascension | **Questie-X-AscensionDB** | [Xurkon/Questie-X-AscensionDB](https://github.com/Xurkon/Questie-X-AscensionDB) |
|
||||
| Project Ebonhold | **Questie-X-EbonholdDB** | [Xurkon/Questie-X-EbonholdDB](https://github.com/Xurkon/Questie-X-EbonholdDB) |
|
||||
| Other / Unknown | Use WotLKDB as a starting baseline, then use QuestieLearner to fill gaps | — |
|
||||
|
||||
#### How to Install the Plugin
|
||||
|
||||
1. Click the repository link for your server in the table above.
|
||||
2. Go to that repo's **Releases** page.
|
||||
3. Download the latest release `.zip`.
|
||||
4. Extract the archive — you will get a folder named `Questie-X-<ServerName>DB` (e.g., `Questie-X-WotLKDB`).
|
||||
5. Move that folder into your `Interface/AddOns/` directory, **alongside** the `Questie-X` folder you installed in Step 1.
|
||||
|
||||
Your final folder structure should look like one of these:
|
||||
|
||||
**For WotLK / Classic / TBC servers:**
|
||||
```
|
||||
World of Warcraft/
|
||||
└── Interface/
|
||||
└── AddOns/
|
||||
├── Questie-X/ ← Step 1 — core addon (REQUIRED)
|
||||
└── Questie-X-WotLKDB/ ← Step 2 — WotLKDB baseline (REQUIRED)
|
||||
```
|
||||
|
||||
**For Ascension servers:**
|
||||
```
|
||||
World of Warcraft/
|
||||
└── Interface/
|
||||
└── AddOns/
|
||||
├── Questie-X/ ← Step 1 — core addon (REQUIRED)
|
||||
├── Questie-X-WotLKDB/ ← WotLKDB baseline (REQUIRED)
|
||||
└── Questie-X-AscensionDB/ ← AscensionDB overrides (REQUIRED)
|
||||
```
|
||||
|
||||
**For Ebonhold servers:**
|
||||
```
|
||||
World of Warcraft/
|
||||
└── Interface/
|
||||
└── AddOns/
|
||||
├── Questie-X/ ← Step 1 — core addon (REQUIRED)
|
||||
├── Questie-X-WotLKDB/ ← WotLKDB baseline (REQUIRED)
|
||||
└── Questie-X-EbonholdDB/ ← EbonholdDB overrides (REQUIRED)
|
||||
```
|
||||
|
||||
> **⚠️ Important:** All folders must be present and at the same level inside `Interface/AddOns/`. The server-specific DB (AscensionDB, EbonholdDB, etc.) goes **alongside** `Questie-X-WotLKDB` — it does not replace it. If any folder is missing, Questie-X will show an error message in chat telling you what's missing.
|
||||
|
||||
#### After Installation
|
||||
|
||||
1. Launch WoW and log in to your character.
|
||||
2. If Questie-X loads successfully, you will see the minimap icon and no error messages.
|
||||
3. If no plugin is detected, Questie-X will print a message in chat telling you exactly which plugin to install.
|
||||
|
||||
#### Note for Unsupported Servers
|
||||
|
||||
If your server is not listed above, install **Questie-X-WotLKDB** as a baseline (it covers general WoW quest data). Then use **QuestieLearner** to record quest and NPC data as you play — gaps will be filled in automatically as you and other players on your server interact with the world. See the [QuestieLearner](#questielearner) section for details.
|
||||
|
||||
---
|
||||
|
||||
## Writing a Plugin
|
||||
|
||||
Questie-X exposes a public `QuestiePluginAPI` that any addon can use to register custom server data without touching core files. This makes it possible to maintain server-specific databases as independent repositories that update on their own release schedule.
|
||||
|
||||
### How It Works
|
||||
|
||||
A plugin calls `QuestiePluginAPI:RegisterPlugin` during addon load and passes its database tables. Questie-X merges these into its runtime database before the first quest scan, so all features — map pins, tooltips, tracker, arrow — work transparently for custom content.
|
||||
|
||||
---
|
||||
|
||||
### Plugin Architecture
|
||||
|
||||
A minimal plugin needs a `.toc` file declaring `Questie-X` as a dependency and a loader script. The `.toc` must list `Questie-X` under `## Dependencies` so the WoW client loads it in the correct order.
|
||||
|
||||
**`MyServer-QuestieDB.toc`**
|
||||
```
|
||||
## Interface: 30300
|
||||
## Title: MyServer QuestieDB
|
||||
## Notes: Quest database plugin for MyServer
|
||||
## Dependencies: Questie-X
|
||||
## Version: 1.0.0
|
||||
|
||||
MyServerLoader.lua
|
||||
MyServerQuestDB.lua
|
||||
MyServerNpcDB.lua
|
||||
MyServerObjectDB.lua
|
||||
MyServerItemDB.lua
|
||||
```
|
||||
|
||||
**`MyServerLoader.lua`**
|
||||
```lua
|
||||
local plugin = QuestiePluginAPI:RegisterPlugin("MyServer")
|
||||
|
||||
-- Inject each database type (tables follow the same schema as Questie's built-in DBs)
|
||||
plugin:InjectDatabase("QUEST", MyServerQuestDB)
|
||||
plugin:InjectDatabase("NPC", MyServerNpcDB)
|
||||
plugin:InjectDatabase("OBJECT", MyServerObjectDB)
|
||||
plugin:InjectDatabase("ITEM", MyServerItemDB)
|
||||
|
||||
-- Optional: inject custom zone/map routing tables
|
||||
plugin:InjectZoneTables(MyServerZoneTables)
|
||||
|
||||
-- Optional: inject fallback UiMapData for non-standard boundary maps
|
||||
plugin:InjectUiMapData(MyServerUiMapData)
|
||||
|
||||
-- Always call this last — clears Questie's internal zone/quest caches
|
||||
-- so freshly injected data is picked up on the next scan
|
||||
plugin:FinishLoading()
|
||||
```
|
||||
|
||||
The database tables follow the same schema as Questie's built-in databases. See [`Modules/Libs/QuestiePluginAPI.lua`](Modules/Libs/QuestiePluginAPI.lua) for the full API reference.
|
||||
|
||||
If your server uses non-standard map data, enable **Options → Advanced → Use WotLK map data** after logging in.
|
||||
|
||||
---
|
||||
|
||||
## Arrow Styles
|
||||
|
||||
Questie-X includes a configurable arrow style picker with preview swatches, plus independent controls for arrow scale, transparency, font size, and objective attachment.
|
||||
|
||||
Bundled arrow assets are detected automatically as either sprite sheets or regular textures, and custom `.tga` files can be dropped into `Icons/Arrows` for use in the dropdown.
|
||||
|
||||
### Arrow Redesign
|
||||
|
||||
- `Arrow1` through `Arrow4` are the bundled image styles, while `arrowold` remains the only bundled sprite sheet.
|
||||
- `Horde Arrow` and `Alliance Arrow` add faction-themed styles with their insignia built into the art.
|
||||
- The dropdown now uses generated preview swatches from `Icons/Arrows`, so each bundled style shows a live thumbnail instead of a text-only entry.
|
||||
- Image arrows rotate as a single texture and keep their native art, while sprite sheets only use sheet-cell logic when the style is explicitly `arrowold` or a custom sheet is marked as such.
|
||||
- The arrow and objective text can be detached or reattached independently, locked separately, and reset with one-click actions.
|
||||
- The attached gap, objective transparency, arrow transparency, font size, and distance unit are all configurable directly from the Arrow tab.
|
||||
- Custom `.tga` files can still be dropped into `Icons/Arrows` and picked from the same dropdown without editing core files.
|
||||
|
||||
---
|
||||
|
||||
## Fixes & Compatibility
|
||||
|
||||
### Quest Log & Tracker
|
||||
|
||||
- Corrected `GetQuestLogTitle` return value indices to match the client API. The client returns `suggestedGroup` at index 4, shifting `isHeader` to index 5 and `questId` to index 9. Previously, modules were using indices 4/8, causing quest headers to be misidentified and `isDaily` to be assigned the wrong value.
|
||||
- Removed premature `break` on `nil` title in quest log iteration loops. Quest log slots on private servers can be non-contiguous; the loop now uses a nil guard instead of aborting, preventing silently skipped quests.
|
||||
- Quest objective counters now update correctly when items are deposited by automated systems that bypass the standard loot frame, using a multi-stage `BAG_UPDATE_DELAYED` strategy.
|
||||
- Fixed `QuestEventHandler` crash on auto-completing quests caused by a missing `QuestiePlayer` module import.
|
||||
- Fixed re-accepted repeatable quests not showing objective icons after the second acceptance.
|
||||
|
||||
### Map & Minimap
|
||||
|
||||
- Fixed `WorldMapFrame` compatibility for servers that render the world map in minimized mode.
|
||||
- Fixed "ghost icon" bug where completed quest icons remained on the map after turn-in.
|
||||
- Fixed `RequestMapUpdate` logic that caused completed quest icons to persist across zone transitions.
|
||||
- Downgraded spurious `[CRITICAL] No AreaId found for UiMapId` log spam to debug level. On some servers, `C_Map.GetBestMapForUnit` returns a continent-level UiMapId for capital cities; the nil return was already handled gracefully but was incorrectly logged as critical.
|
||||
- Fixed map pins for `killCredit`-type objectives not resolving spawn locations correctly.
|
||||
- Added a configurable minimap icon range cutoff so players can choose how far away quest icons remain visible before fading out.
|
||||
|
||||
### Tooltips
|
||||
|
||||
- Fixed `attempt to concatenate nil` error when a quest starter or finisher has no name in the database.
|
||||
- Added support for `killcredit` and `spell` objective types in `MapIconTooltip`.
|
||||
- Tooltip now displays if an NPC drops an item that starts a quest.
|
||||
- Fixed `attempt to concatenate local 'minLevel' (a nil value)` crash in `MapIconTooltip` when hovering over creatures whose `creatureLevels` entry was an empty table instead of the expected `{minLevel, maxLevel, rank}` tuple. Added early-return guard in `_GetLevelString`.
|
||||
- Fixed tooltip crash when hovering over NPC/object keys (`m_<id>`, `o_<id>`) where `learnedNpc[10]` or `learnedObj[10]` is unexpectedly a string instead of a table. Added type guard before iterating the objective list array.
|
||||
|
||||
### Quest Arrow
|
||||
|
||||
- Refactored distance calculations and target prioritisation; arrow now correctly filters targets by zone and instance.
|
||||
- Fixed arrow pointing to previously completed objective locations instead of the current finisher.
|
||||
- Fixed nil error in `_CollectObjective` when processing incomplete quests.
|
||||
- Fixed arrow direction for quests that require speaking to an NPC as a prerequisite step.
|
||||
- **Sunstrider Isle (Ascension starting zone)**: Resolved arrow distance, direction, and map pin issues on Sunstrider Isle. `GetCurrentZoneId()` can return 3430 OR 3431 when the player is on uiMap 1241 — all 4 Sunstrider detection checks now accept both zoneIds plus uiMapId 1241. Fixed arrow rotation direction (`SetRotation` is CW-positive, not CCW). Fixed collection function distance mismatch where targets were converted through 1941 (Eversong) bounds while player coords were in 1241 bounds. NPC 15281 spawn zone corrected from 3430 to 1241 so coords land in Sunstrider's normalized space. Removed the 1241→1941 redirect in `_ResolveMapUiMapId()`; pins on 1241 now render natively via `areaIdToUiMapId[1241] = 1241`.
|
||||
|
||||
### Nameplates
|
||||
|
||||
- Questie nameplate hooks are skipped when a conflicting nameplate addon is detected, preventing taint and UI errors.
|
||||
|
||||
### Databases & Custom IDs
|
||||
|
||||
- Full support for large integer NPC, quest, object, and item IDs used by custom server emulators.
|
||||
- Fixed `ZoneDB` crash when encountering maps with no AreaId mapping (e.g. continent-level maps on Kalimdor).
|
||||
- Fixed `GetObject` returning nil for Item Finishers misidentified as GameObject Finishers on custom servers.
|
||||
- Fixed `NPC 30514` (Thorim listen bunny) missing fallback spawn data for Sibling Rivalry turn-in.
|
||||
|
||||
---
|
||||
|
||||
## QuestieLearner
|
||||
|
||||
QuestieLearner is Questie-X's built-in crowdsourced database system. When you interact with the world — accepting quests, killing objectives, looting items, interacting with objects — Questie-X silently records any data it doesn't already have (spawn locations, NPC IDs, item IDs, coordinates). That data is saved locally per-realm and can be shared with other players or submitted back to improve the database for everyone.
|
||||
|
||||
This is the primary tool for filling in gaps on **custom or lightly-documented servers** where the base database plugin doesn't have complete coverage.
|
||||
|
||||
### What It Learns Automatically
|
||||
|
||||
QuestieLearner hooks into several in-game events and records data passively without any user action:
|
||||
|
||||
| Event | What is learned |
|
||||
|-------|----------------|
|
||||
| Quest accept / turn-in | Quest ID → quest giver/finisher NPC position and ID |
|
||||
| Kill objective progress | NPC ID → spawn zone and coordinates at time of kill |
|
||||
| Object interaction | Object ID → spawn zone and coordinates |
|
||||
| Item loot | Item ID → which NPC/object it dropped from |
|
||||
| Mouseover | NPC/object name resolved from server for any entity you hover |
|
||||
|
||||
Data for quest IDs that exist in the base database is merged into the existing entry. Data for unknown quest IDs (custom server content not yet in the plugin) is stored separately under a per-realm key so it doesn't contaminate the base data.
|
||||
|
||||
### Sharing Data with Nearby Players
|
||||
|
||||
If other players nearby are also running Questie-X, learned entries are broadcast automatically via the `QUESTIE_LEARNER` addon message channel. You receive their data and they receive yours — no configuration required. This means a group running the same zone will collectively fill in the map faster than any single player could alone.
|
||||
|
||||
Received data is validated before merging: entries missing zone ID, coordinates, or NPC name are discarded.
|
||||
|
||||
### Exporting Your Data
|
||||
|
||||
Once you've accumulated learned data you want to share (e.g. to contribute back to the plugin repository or send to another player), export it from the **Options → Database** tab:
|
||||
|
||||
1. Open Questie-X options: `/questie` or click the minimap button → **Options**.
|
||||
2. Go to the **Database** tab.
|
||||
3. Click **Export**. A compressed, base64-encoded string is generated and shown in the text box.
|
||||
4. Copy the entire string (Ctrl+A → Ctrl+C).
|
||||
|
||||
The export string encodes your full `QuestieLearnerDB` for the current realm using LibDeflate. It is safe to paste into a Discord message, GitHub issue, or pastebin.
|
||||
|
||||
### Importing Data
|
||||
|
||||
To load data exported by another player or provided by the community:
|
||||
|
||||
1. Open Questie-X options → **Database** tab.
|
||||
2. Paste the export string into the import text box.
|
||||
3. Click **Import**. Questie-X decodes and merges the data into your local database and immediately injects it into the active override tables — map pins and tracker entries update without a full reload. A `/reload` is only required if you want newly imported quest starters/finishers to appear on the world map for quests already in your log.
|
||||
|
||||
Imported entries follow the same validation rules as received broadcast entries. Conflicts (same NPC ID with different coordinates) are resolved by keeping the entry with the most data fields populated.
|
||||
|
||||
### Cleaning Up Stale Data
|
||||
|
||||
Over time, the learned database can accumulate entries from old patches, removed NPCs, or incorrect data from unreliable sources. To prune it:
|
||||
|
||||
1. Open Questie-X options → **Database** tab.
|
||||
2. Click **Cleanup**. This removes entries where the NPC/object/item no longer exists in the current loaded database or has coordinates that fall outside any known zone boundary.
|
||||
|
||||
### Contributing Learned Data to a Plugin
|
||||
|
||||
If you've accumulated significant data for a custom server that doesn't yet have a plugin (or has an incomplete one):
|
||||
|
||||
1. Export your data as above.
|
||||
2. Open an issue or pull request on the relevant plugin repository (e.g. [Questie-X-AscensionDB](https://github.com/Xurkon/Questie-X-AscensionDB)) and paste your export string.
|
||||
3. The maintainer can decode it with the same Import function and integrate confirmed entries into the next release.
|
||||
|
||||
### Slash Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/questie` | Open the options panel (navigate to Database tab) |
|
||||
| `/ql export` | Print the export string directly to chat for quick copy |
|
||||
| `/ql import <string>` | Import a data string without opening the options panel |
|
||||
| `/ql clear` | Clear all learned data for the current realm |
|
||||
| `/ql status` | Print a summary of how many entries have been learned per data type |
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Visual Map Objectives
|
||||
|
||||
Quest starters, turn-ins, and all objective types are drawn as icons directly on the minimap and world map.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/4abi5yu.png" height="200" alt="Quest Givers" />
|
||||
<img src="https://i.imgur.com/DgvBHyh.png" height="200" alt="Quest Complete" />
|
||||
<img src="https://i.imgur.com/uPykHKC.png" height="200" alt="Quest Tooltip" />
|
||||
</div>
|
||||
|
||||
### Quest Tracker
|
||||
|
||||
- Tracks quests automatically on acceptance.
|
||||
- Displays up to 20 quests simultaneously (original limit: 5).
|
||||
- Left-click opens the quest log; right-click provides focus mode and TomTom arrow integration.
|
||||
- Headers persist correctly across all session events.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8838573/67285596-24dbab00-f4d8-11e9-9ae1-7dd6206b5e48.png" width="400" alt="Tracker" />
|
||||
</div>
|
||||
|
||||
### Quest Arrow
|
||||
|
||||
Directional arrow pointing toward the nearest active objective or quest finisher, with zone and instance awareness.
|
||||
|
||||
### My Journey & Quests by Zone
|
||||
|
||||
- **Journey Log** — records every quest accepted, completed, and abandoned during a session.
|
||||
- **Quests by Zone** — lists all available and completed quests in a given zone for completionists.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8838573/67285651-3cb32f00-f4d8-11e9-95d8-e8ceb2a8d871.png" height="200" alt="Journey" />
|
||||
<img src="https://user-images.githubusercontent.com/8838573/67285665-450b6a00-f4d8-11e9-9283-325d26c7c70d.png" height="200" alt="Zone Quests" />
|
||||
</div>
|
||||
|
||||
### Database Search & Configuration
|
||||
|
||||
- Search the full Questie database for any NPC, object, or quest by name or ID.
|
||||
- Extensive options: icon scale, tracking behaviour, nameplate display, tracker layout, and more.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8838573/67285691-4f2d6880-f4d8-11e9-8656-b3e37dce2f05.png" height="200" alt="Search" />
|
||||
<img src="https://user-images.githubusercontent.com/8838573/67285731-61a7a200-f4d8-11e9-9026-b1eeaad0d721.png" height="200" alt="Config" />
|
||||
</div>
|
||||
|
||||
---
|
||||
- implemented Ascension's backported supertracker buttons to the tracker.
|
||||
- added "By Zone + % Completed" and "By Zone + % Completed (Reversed)" tracker sorting options.
|
||||
|
||||
## Credits
|
||||
|
||||
- **Questie Team** — Original addon developers.
|
||||
- **Xurkon** — Questie-X fork and ongoing maintenance.
|
||||
- **[Majed (3majed)](https://github.com/3majed/Questie-335)** — Ascension server dataset.
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see [LICENSE](LICENSE) for details.
|
||||
[aron-w/Questie-X](https://github.com/aron-w/Questie-X)<br>
|
||||
[Xurkon/Questie-X](https://github.com/Xurkon/Questie-X)<br>
|
||||
[Questie](https://github.com/Questie/Questie)
|
||||
|
||||
Reference in New Issue
Block a user