From 5bee40b953558239ebd5116d1b4e50ae3f878d45 Mon Sep 17 00:00:00 2001 From: Xurkon <36556990+Xurkon@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:09:21 -0600 Subject: [PATCH] chore: release v9.7.12 - Fixed missing Finisher frame pool reset - Fixed tracker force-complete race condition on custom quests --- .history/CHANGELOG_20260222000716.md | 137 ++ .../QuestieFramePool_20260116015930.lua | 397 ++++ .../QuestieFramePool_20260222000626.lua | 398 ++++ .../Quest/QuestieQuest_20260222000236.lua | 1728 +++++++++++++++++ .history/Questie-335_20260222000735.toc | 250 +++ .history/README_20260222000744.md | 105 + CHANGELOG.md | 7 + Modules/FramePool/QuestieFramePool.lua | 1 + Modules/Quest/QuestieQuest.lua | 3 +- Questie-335.toc | 2 +- README.md | 2 +- 11 files changed, 3027 insertions(+), 3 deletions(-) create mode 100644 .history/CHANGELOG_20260222000716.md create mode 100644 .history/Modules/FramePool/QuestieFramePool_20260116015930.lua create mode 100644 .history/Modules/FramePool/QuestieFramePool_20260222000626.lua create mode 100644 .history/Modules/Quest/QuestieQuest_20260222000236.lua create mode 100644 .history/Questie-335_20260222000735.toc create mode 100644 .history/README_20260222000744.md diff --git a/.history/CHANGELOG_20260222000716.md b/.history/CHANGELOG_20260222000716.md new file mode 100644 index 0000000..a25c407 --- /dev/null +++ b/.history/CHANGELOG_20260222000716.md @@ -0,0 +1,137 @@ +# Changelog + +## v9.7.12 + +### Fixes +- **[Quest]** Fixed an issue where tracking icons for dynamically updated custom quests like Peak Predators would prematurely disappear due to sync delays on WotLK servers. +- **[Map]** Resolved a frame pool leak that prevented the yellow Finisher icon from appearing immediately on the map after turning in or completing a quest. + + +## v9.7.11 + +### Fixes +- **[Quest]** Fixed an issue where quest objective icons and waypoints failed to clear from the map after a quest was completed or abandoned. +- **[Tracker]** Prevented quests from falsely flagging as complete and wiping tracking data when the World of Warcraft server drops its objective arrays. Fixes "Peak Predators" icons disappearing randomly. +- **[Database]** Corrected an error that improperly identified Item Finishers as GameObject Finishers on custom servers, causing map pinpointing errors (e.g. `[QuestieDB:GetObject] rawdata is nil for objectID:`). +- **[Database]** Appended correct fallback spawn data for the "Thorim" listen bunny (`NPC 30514`) so Sibling Rivalry's turn in/listen point functions correctly on WotLK clients. + +## v9.7.10 + +### Fixes +- **[Arrow]** Refactored Arrow logic to drastically improve target distance calculations and prioritize targets correctly based on the player's current zone. +- **[Arrow]** Fixed a bug where the Arrow would mistakenly point to previously completed objective locations instead of the Quest Finisher's exact location. +- **[Tracker]** Fixed `QuestieDB.IsComplete` edge case returning incomplete incorrectly; now verifies `numFulfilled == numRequired` to immediately acknowledge completed quests while awaiting the server flag. +- **[Tracker]** Resolved false-positive "broken quest log" errors spamming chat on WotLK servers by correctly handling API responses for trackable objectives. +- **[Tracker]** Demoted harmless WotLK objective cache count mismatches from Error to Debug visibility level to eliminate chat spam on login. +- **[Quest]** Fixed quest arrow pointing to the key-drop NPC after using all consumable quest keys (e.g. Cold Iron Key for "They Took Our Men!" quest 12843). When a quest uses a key item that is consumed on interaction, it leaves the bag and `CheckQuestSourceItem` returns false, incorrectly triggering a quest reset that re-drew the key source NPC on the map. Now checks if all tracked objectives are already `Completed=true` before applying the reset, preventing the spurious icon. + +## v9.7.9 + +### Fixes +- **[Quest]** Fixed tracker objective count staying stale (e.g. stuck at 14/16 when quest is complete) when an autoloot bot bypasses the standard loot frame. Registered `BAG_UPDATE_DELAYED` event to force a full quest log scan on bag changes, catching progress updates that `QUEST_WATCH_UPDATE` would normally fire for manual looting. + +## v9.7.8 + +### Fixes +- **[Quest]** Fixed "There was an error populating objectives" error in chat for `triggerEnd` quests with no map coordinates (e.g., "complete N quests in zone"). `_RegisterObjectiveTooltips` now silently returns for `event`-type objectives with no `spawnList` — these have nothing to register a tooltip for. + +## v9.7.7 + +### Fixes +- **[Quest]** Fixed "Missing event data for Objective" error appearing in chat for all `triggerEnd` quests with no map coordinates (e.g., "complete N quests in zone" types). Nil coordinates are valid for server-tracked objectives with no pin — the handler now returns silently instead of logging a visible error. + +## v9.7.6 + +### Fixes +- **[Database]** Fixed "Missing objective data" error for all "complete N quests in zone" quest types (IDs 50151, 50145, 50098, 50100, 50149, 50099, 50108, 50111, 50150). Added `triggerEnd` (`[9]`) field so Questie correctly registers the server-tracked objective without drawing map pins. + +## v9.7.5 + +### Fixes +- **[Quest]** Fixed objective pins/icons persisting on the map after a quest is completed or abandoned. Added `CleanupRemovedQuestsFallback` which diffs Questie's quest log against the game's actual quest log on every `QUEST_LOG_UPDATE` and correctly calls `CompleteQuest` or `AbandonedQuest` for any quest that silently disappeared, ensuring map icons are removed. +- **[Quest]** Fixed re-accepted repeatable custom quests (e.g., Stormforged Scales) not showing objective icons on the map after being accepted a second time. + +### New Quests +- **[Database]** Added **Storm Peak Orders** (ID 50150) - *The Storm Peaks* + - Objective: Complete any 6 quests in The Storm Peaks. +- **[Database]** Added **Wild Basin** (ID 50094) - *Sholazar Basin* + - Objective: Kill 75 Beasts. Includes 29 Beast NPC types with full spawn coordinates. + - NPCs: King Krush, Shardhorn Rhino, Aotona, Pitch, Serfex the Reaver, Dreadsaber, Hardknuckle Matriarch, Shango, Venomtip, Bushwhacker, Hardknuckle Charger, Ravenous Mangal Crocolisk, Farunn, Zeptek the Destroyer, Goretalon Matriarch, Sapphire Hive Wasp, Emperor Cobra, Sapphire Hive Drone, Shattertusk Bull, Siltslither Eel, Spirit of Atha, Stranded Thresher, Mangal Crocolisk, Spirit of Koosu, Longneck Grazer, Goretalon Roc, Sapphire Hive Queen, Spirit of Ha-Khalan, Bittertide Hydra + +## v9.7.4 + +### Fixes +- **[Tooltips]** Fixed "attempt to concatenate local 'name' (a nil value)" error when quest starters/finishers have missing names in the database. +- **[Database]** Added missing spawn coordinates for Quest 50031 "Stormbound" elementals in Storm Peaks (zone 67). +- **[Database]** Fixed "Unknown Zone" issue for custom quests by correcting Zone ID index usage (swapped `[6]` RequiredRaces for `[17]` ZoneID). +- **[Database]** Corrected Dragonblight Zone ID in custom quest definitions. + +### New Quests +- **[Database]** Added **Morogh Missions** (ID 50098) - *Dun Morogh* + - Objective: Complete any 6 quests in Dun Morogh. Auto-completes upon reaching the objective. +- **[Database]** Added **Azuremyst Aid** (ID 50100) - *Azuremyst Isle* + - Objective: Complete any 6 quests in Azuremyst Isle. Auto-completes upon reaching the objective. +- **[Database]** Added **Stormforged Scales** (ID 50066) - *The Storm Peaks* + - Objective: Kill 30 Dragonkin. Includes 8 Dragonkin NPC types with full spawn coordinates. +- **[Database]** Added **Peak Predators** (ID 50095) - *The Storm Peaks* + - Objective: Kill 75 Beasts. Includes 24 Beast NPC types with full spawn coordinates. +- **[Database]** Added **Peak Predators** (ID 50096) - *Icecrown* + - Objective: Kill 75 Beasts. Includes 12 Beast NPC types with full spawn coordinates. +- **[Database]** Added **Icecrown Advance** (ID 50151) - *Icecrown* + - Objective: Complete any 6 quests in Icecrown. Auto-completes upon reaching the objective. +- **[Database]** Added **Storm Peaks Trophy** (ID 50205) - *The Storm Peaks* + - Objective: Kill 1 Rare in The Storm Peaks. Includes 4 Rare NPC types (Skoll, Time-Lost Proto-Drake, Vyragosa, Dirkee) with full spawn coordinates. + +## v9.7.3 + +### New Features +- **[Database]** Implemented **Ebonhold Database Module**. + - Created dedicated `Database/Ebonhold/` structure for custom server data. + - Added `EbonholdLoader` to inject custom Quests, NPCs, Objects, and Items as overrides. + - **Note:** This structure preserves custom data during upstream Questie updates. +- **[Objectives]** Implemented **Automated Text Retrieval**. + - Questie now attempts to fetch quest text from the server at runtime for custom quests that are missing from the database. + - Added "Objectives Board" (ID 600600) as a global quest starter. + +### Quests (Custom Content) +- **[New]** Added **Heart of the Dragonflights** (ID 50064) - *Dragonblight* + - Objective: Kill 30 Dragonkin. Includes 49 Dragonkin NPC types. +- **[New]** Added **Skies of Blade's Edge** (ID 50060) - *Blade's Edge Mountains* + - Objective: Kill 75 Dragonkin. Includes 17 Dragonkin NPC types. +- **[New]** Added **Shadowed Beasts** (ID 50087) - *Shadowmoon Valley* + - Objective: Kill 75 Beasts. Includes 25 Beast NPC types. +- **[New]** Added **Forest Stalkers** (ID 50083) - *Terokkar Forest* + - Objective: Kill 75 Beasts. Includes 53 Beast NPC types. +- **[New]** Added **Savage Heights** (ID 50085) - *Blade's Edge Mountains* + - Objective: Kill 75 Beasts. Includes 47 Beast NPC types. +- **[New]** Added **Unstable Fauna** (ID 50086) - *Netherstorm* + - Objective: Kill 75 Beasts. Includes 18 Beast NPC types. +- **[New]** Added **Elemental Balance** (ID 50026) - *Nagrand* + - Objective: Kill 30 Elementals. Includes 12 Elemental NPC types. +- **[New]** Added **Redridge Trophy** (ID 50160) and **Zangarmarsh Trophy** (ID 50192). + +### New Ascension Quests +- **Westfall**: Agria's Medicine, Seven Years of Bad Luck, Worm-Eaten Apple, Goldshire's Generosity, Bookworm, Knowledge Corrupts, The Ruins of Northshire, Accursed Sisterhood, Words That Shepherd Madness, Oracular Idol, A Betrayal Within, The Maid I Left Behind, The Saddest Among Us, The Threat Swept Downstream, Stay a While, Defias Disruption. +- **Dun Morogh**: A Small Mistake, We Found Her!, The Scout's Favor, Old Mirsinth, Smoke on the Wind, A Promising Path, A Fitting Disguise, His Radiant Majesty, Deciphering Radiation, Soaking the Masses, Sever the Right Hand, A Growing Business, Thunderbrew's Hop, Stay a While, Live-Fire Demo, Bots on Strike, A Brother's Betrayal, The True Story, Timber for the Coldhewn, Icehide the Unbroken. +- **Teldrassil**: The Carrion Road, The Sister Who Never Returned, Finding the Good Meat, Transsubstantiating the Flesh, Communion Banquet, A Trail of Petals, Restless entrails, A Dark Warning, The Aid of Theren-Dion, No Place for Scavengers, Termites in Teldrassil, Stay a While, Elydna's Heirloom. +- **Durotar**: To Find a Cure, A Dangerous Sample, Knowledge of the Centaurs, Those Who Fell, The Way is Shut, The Sinister Triad, So That He May Hear Again, A Sinister Ritual, Innocents for Sinners, Unease Makes Tongues Wag, A Door Left Ajar, Esgramor's Master, Shinies!, Echoes of Hirsutta, Auction the Past, Stay a While, Durotar's Dire Drought, The Queen's Decree, Avianna's Rose, The Last Piece, Reversion. +- **Tirisfal Glades**: Rude Awakening, Marla's Last Wish, Monsters With Noble Intentions, Restless Family Members, An Unspeakable Secret, A Noble Heritage, The True Heir of the Cains, The Friends We Make Along the Way, I'm Home, Apothecary Flemer, The Nature of Freedom, Spotless Standing, Stay a While, More Than the Sum of its Parts, Scarlet Correspondence, A Humble Duty, The Balnirs' Rest, Brewing Disarray, This Is Justice. +- **Mulgore**: Death and Tribute, Death and Exile, Death and Dishonor, Death and Justice, Death by Laughter, To Whom I Devote, Fighting Over Carrion, Smoke on the Horizon, Amphora of Sacred Water, Stay a While, Exile of Embers, The Smoke that Remembers, The Circle’s Rite. + +### New Ebonhold Quests +- **Outland**: Elemental Balance, Savage Heights, Unstable Fauna, Forest Stalkers, Marsh Predators, Skies of Blade's Edge, Shadowed Beasts, Zangarmarsh Trophy. +- **Northrend**: Tundra Turbulence, Stormbound, Dragonblight Trophy, Heart of the Dragonflights, Peak Predators, Icecrown Advance, Storm Peaks Trophy, Stormforged Scales, Fjord Front, Wild Basin, Grizzly Trophy, Zul'Drak Trophy, Basin Expeditions. +- **Azeroth**: Redridge Trophy, Morogh Missions, Azuremyst Aid, Shadow of Teldrassil, Trials of Durotar, Song of the Woods, Morogh Trophy, Tirisfal Trophy. + +### Fixes +- **[Tracker]** **Combat Update Fix**: Tracker now updates objectives immediately during combat without causing Lua errors or taint. +- **[Tracker]** **Bag Update Fix**: Quest progress now updates immediately when looting items (fixes delay with loot bots). +- **[Arrow]** **Refined Visibility Logic**: + - **Auto Nearby**: Arrow correctly defaults to showing the nearest quest when no quests are tracked. + - **Zone Filter**: In "Auto Mode", the arrow hides if the nearest quest is in a different zone. + - **Instance Filter**: Arrow explicitly hides if the target is in a different instance. +- **[Map]** Fixed an issue where completed quest icons would persist on the map (`RequestMapUpdate` logic). +- **[Database]** Updated `wotlkNpcDB.lua` with scraped spawn data for 12 key beast NPCs in Terokkar Forest to ensure accuracy. +- **[Arrow]** Fixed a nil function error for `_CollectObjective` when processing incomplete quests. +- **[Arrow]** Fixed syntax issues that prevented `QuestieArrow` module from initializing correctly. +- **[Database]** Fixed a runtime crash in `ZoneDB` when encountering maps with no AreaId mapping (e.g., Kalimdor). diff --git a/.history/Modules/FramePool/QuestieFramePool_20260116015930.lua b/.history/Modules/FramePool/QuestieFramePool_20260116015930.lua new file mode 100644 index 0000000..0ed393d --- /dev/null +++ b/.history/Modules/FramePool/QuestieFramePool_20260116015930.lua @@ -0,0 +1,397 @@ +---@class QuestieFramePool +local QuestieFramePool = QuestieLoader:CreateModule("QuestieFramePool") +------------------------- +--Import modules. +------------------------- +---@type QuestieQuest +local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest") +---@type QuestieMap +local QuestieMap = QuestieLoader:ImportModule("QuestieMap") +---@type MapIconTooltip +local MapIconTooltip = QuestieLoader:ImportModule("MapIconTooltip") +---@type QuestieLib +local QuestieLib = QuestieLoader:ImportModule("QuestieLib") +---@type l10n +local l10n = QuestieLoader:ImportModule("l10n") + +--- COMPATIBILITY --- +local C_Timer = QuestieCompat.C_Timer +local WorldMapFrame = QuestieCompat.WorldMapFrame + +local HBDPins = QuestieCompat.HBDPins or LibStub("HereBeDragonsQuestie-Pins-2.0") + +-- set pins parent to QuestieFrameGroup for easier compatibility with other addons +-- cant use this because it fucks with everything, but we gotta stick with HereBeDragonsQuestie anyway +HBDPins.MinimapGroup = CreateFrame("Frame", "QuestieFrameGroup", Minimap) +local WAYPOINT_COLOR = { 1, 0.72, 0, 0.5} + +local _QuestieFramePool = {} +local numberOfFrames = 0 + +local unusedFrames = {} +local usedFrames = {}; +local allFrames = {} + +StaticPopupDialogs["QUESTIE_CONFIRMHIDE"] = { + text = "", -- set before showing + questID = 0, -- set before showing + button1 = l10n("Yes"), + button2 = l10n("No"), + OnAccept = function() + QuestieQuest:HideQuest(StaticPopupDialogs["QUESTIE_CONFIRMHIDE"].questID) + end, + SetQuest = function(self, id) + self.questID = id + self.text = l10n("Are you sure you want to hide the quest '%s'?\nIf this quest isn't actually available, please report it to us!", QuestieLib:GetColoredQuestName(id, Questie.db.profile.enableTooltipsQuestLevel, false, true)) + + -- locale might not be loaded when this is first created (this does happen almost always) + self.button1 = l10n("Yes") + self.button2 = l10n("No") + end, + OnShow = function(self) + self:SetFrameStrata("TOOLTIP") + end, + timeout = 0, + whileDead = true, + hideOnEscape = true, + preferredIndex = 3 +} + +-- Global Functions -- +---@return IconFrame +function QuestieFramePool:GetFrame() + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:GetFrame]") + + ---@type IconFrame + local returnFrame = next(unusedFrames) + returnFrame = returnFrame and unusedFrames[returnFrame] + + if returnFrame and returnFrame.frameId and usedFrames[returnFrame.frameId] then + -- something went horribly wrong (desync bug?) don't use this frame since its already in use + Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieFramePool:GetFrame] Tried to reuse frame, but that frame is already in use. frameId:", returnFrame.frameId) + returnFrame = nil + end + if not returnFrame then + returnFrame = _QuestieFramePool:QuestieCreateFrame() + else + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:GetFrame] Reusing frame") + unusedFrames[returnFrame.frameId] = nil + end + if returnFrame ~= nil and returnFrame.hidden and returnFrame._show ~= nil and returnFrame._hide ~= nil then -- restore state to normal (toggle questie) + returnFrame.hidden = false + returnFrame.Show = returnFrame._show; + returnFrame.Hide = returnFrame._hide; + returnFrame._show = nil + returnFrame._hide = nil + end + returnFrame.FadeLogic = nil + returnFrame.faded = nil + returnFrame.miniMapIcon = nil + + returnFrame.data = nil + returnFrame.x = nil; + returnFrame.y = nil; + returnFrame.AreaID = nil; + returnFrame.UiMapID = nil + + if returnFrame.texture then + returnFrame.texture:SetVertexColor(1, 1, 1, 1) + end + returnFrame.loaded = true + returnFrame.shouldBeShowing = nil + returnFrame.hidden = nil + + if returnFrame.BaseOnShow then + returnFrame:SetScript("OnShow", returnFrame.BaseOnShow) + end + + if returnFrame.BaseOnUpdate then + returnFrame.glowLogicTimer = C_Timer.NewTicker(1, returnFrame.BaseOnUpdate); + else + returnFrame:SetScript("OnUpdate", nil) + end + + if returnFrame.BaseOnHide then + returnFrame:SetScript("OnHide", returnFrame.BaseOnHide) + end + + usedFrames[returnFrame.frameId] = returnFrame + return returnFrame +end + +function QuestieFramePool:UpdateGlowConfig(mini, mode) + if mode then + for _, icon in pairs(usedFrames) do + if (((mini and icon.miniMapIcon) or not mini) and icon.glow) and icon.IsShown and icon:IsShown() then + icon:GetScript("OnShow")(icon) -- forces a glow update + end + end + else + for _, icon in pairs(usedFrames) do + if ((mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon)) and icon.glow then + icon.glow:Hide() + end + end + end +end + +function QuestieFramePool:UpdateColorConfig(mini, enable) + if enable then + for _, icon in pairs(usedFrames) do + if (mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon) then + local colors = {1, 1, 1} + if icon.data.IconColor ~= nil then + colors = icon.data.IconColor + end + icon.texture:SetVertexColor(colors[1], colors[2], colors[3], 1) + end + end + else + for _, icon in pairs(usedFrames) do + if (mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon) then + icon.texture:SetVertexColor(1, 1, 1, 1) + end + end + end +end + +function QuestieFramePool:RecycleFrame(frame) + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:RecycleFrame]") + usedFrames[frame.frameId] = nil + unusedFrames[frame.frameId] = frame +end + +function _QuestieFramePool:QuestieCreateFrame() + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:QuestieCreateFrame]") + numberOfFrames = numberOfFrames + 1 + local newFrame = QuestieFramePool.Qframe:New(numberOfFrames, MapIconTooltip.Show) + + tinsert(allFrames, newFrame) + return newFrame +end + + +---@param iconFrame IconFrame @The parent frame for the current line. +---@param waypointTable table @A table containing waypoints {{X, Y}, ...} +---@param lineWidth number @Width of the line. +---@param color number[] @A table consisting of 4 variable {1, 1, 1, 1} RGB-Opacity +---@return LineFrame[] +function QuestieFramePool:CreateWaypoints(iconFrame, waypointTable, lineWidth, color, areaId) + local lineFrameList = {} + local lastPos + --Set defaults if needed. + local lWidth = lineWidth or 1.5; + local col = color or WAYPOINT_COLOR + + for _, waypointSubTable in pairs(waypointTable) do + lastPos = nil + for _, waypoint in pairs(waypointSubTable) do + if lastPos then + local lineFrame = QuestieFramePool:CreateLine(iconFrame, lastPos[1], lastPos[2], waypoint[1], waypoint[2], lWidth, col, areaId) + tinsert(lineFrameList, lineFrame); + end + lastPos = waypoint + end + end + return lineFrameList; +end + +local lineFrameCount = 1 + +---@param iconFrame IconFrame @The parent frame for the current line. +---@param startX number @A value between 0-100 +---@param startY number @A value between 0-100 +---@param endX number @A value between 0-100 +---@param endY number @A value between 0-100 +---@param lineWidth number @Width of the line. +---@param color number[] @A table consisting of 4 variable {1, 1, 1, 1} RGB-Opacity +---@return LineFrame +---@class LineFrame @A frame that contains the line used in waypoints. +function QuestieFramePool:CreateLine(iconFrame, startX, startY, endX, endY, lineWidth, color, areaId) + + --Create the framepool for lines if it does not already exist. + if not QuestieFramePool.Routes_Lines then + QuestieFramePool.Routes_Lines={} + end + --Names are not stricktly needed, but it is nice for debugging. + local frameName = "questieLineFrame".. lineFrameCount; + + --tremove default always picks the last element, however counting arrays is kinda bugged? So just get index 1 instead. + local lineFrame = tremove(QuestieFramePool.Routes_Lines, 1) or CreateFrame("Button", frameName, iconFrame); + if not lineFrame.frameId then + lineFrame.frameId = lineFrameCount; + end + + local canvas = WorldMapFrame:GetCanvas() + + local width = canvas:GetWidth(); + local height = canvas:GetHeight(); + + --Setting the parent is required to get the correct frame levels. + + lineFrame:SetParent(canvas) --This fixes the pan and zoom for lines + if QuestieCompat.Is335 then + lineFrame.CreateLine = QuestieCompat.CreateLine + else + lineFrame:SetFrameLevel(2015) -- This needs to be high, because of the regular WorldMapFrame.ScrollContainer + lineFrame:SetFrameStrata("FULLSCREEN") + end + + --How to identify what the frame actually contains, this is not used atm could easily be changed. + lineFrame.type = "line" + + --Include the line in the iconFrame. + if not iconFrame.data.lineFrames then + iconFrame.data.lineFrames = {}; + end + tinsert(iconFrame.data.lineFrames, lineFrame); + lineFrame.iconFrame = iconFrame; + lineFrame.data = iconFrame.data + lineFrame.x = (startX + endX) / 2 + lineFrame.y = (startY + endY) / 2 + lineFrame.AreaID = areaId or iconFrame.AreaID + lineFrame.texture = iconFrame.texture + + function lineFrame:Unload() + if not self.iconFrame then + return -- already unloaded + end + self:Hide(); + self.iconFrame = nil; + self.x = nil + self.y = nil + self.data = nil + self.texture = nil + self.AreaID = nil + HBDPins:RemoveWorldMapIcon(Questie, self) + tinsert(QuestieFramePool.Routes_Lines, self); + end + local line = lineFrame.line or lineFrame:CreateLine(); + lineFrame.line = line; + + line.dR = color[1]; + line.dG = color[2]; + line.dB = color[3]; + line.dA = color[4]; + line:SetColorTexture(color[1],color[2],color[3],color[4]); + + local lineBorder = lineFrame.lineBorder or lineFrame:CreateLine(); + lineFrame.lineBorder = lineBorder; + + lineBorder.dR = color[1]; + lineBorder.dG = color[2]; + lineBorder.dB = color[3]; + lineBorder.dA = color[4]; + lineBorder:SetColorTexture(0,0,0,color[4]/2); + + -- Set texture coordinates and anchors + --line:ClearAllPoints(); + + startX = startX * width / 100 + startY = startY * height / -100 -- We do by / -100 due to using the top left point + endX = endX * width / 100 + endY = endY * height / -100 + + width = abs(startX - endX) + lineWidth * 4 + height = abs(startY - endY) + lineWidth * 4 + + local framePosX = max(startX, endX) - lineWidth * 2 - width / 2 + local framePosY = min(startY, endY) + lineWidth * 2 + height / 2 + + lineFrame:SetHeight(height); + lineFrame:SetWidth(width); + lineFrame:SetPoint("TOPLEFT", canvas, "TOPLEFT", framePosX, framePosY) + + line:SetDrawLayer("OVERLAY", -5) + line:SetStartPoint("TOPLEFT", startX - framePosX, startY - framePosY) + line:SetEndPoint("TOPLEFT", endX - framePosX, endY - framePosY) + line:SetThickness(lineWidth); + + lineBorder:SetDrawLayer("OVERLAY", -6) + lineBorder:SetStartPoint("TOPLEFT", startX - framePosX, startY - framePosY) + lineBorder:SetEndPoint("TOPLEFT", endX - framePosX, endY - framePosY) + lineBorder:SetThickness(lineWidth+2); + + lineFrame:EnableMouse(true) + + --- This is needed because HBD will show the icons again after switching zones and stuff like that + function lineFrame:FakeHide() + if not self.hidden then + self.shouldBeShowing = self:IsShown(); + self._show = self.Show; + self.Show = function() + self.shouldBeShowing = true; + end + self:Hide(); + self._hide = self.Hide; + self.Hide = function() + self.shouldBeShowing = false; + end + self.hidden = true + end + end + + --- This is needed because HBD will show the icons again after switching zones and stuff like that + function lineFrame:FakeShow() + if self.hidden then + self.hidden = false + self.Show = self._show; + self.Hide = self._hide; + self._show = nil + self._hide = nil + if self.shouldBeShowing then + self:Show(); + end + end + end + + --lineFrame:SetBackdrop({ -- mouseover debugging + -- bgFile = "Interface/Tooltips/UI-Tooltip-Background", + -- edgeFile = nil, + -- edgeSize = 0, + -- insets = { left = 0, right = 0, top = 0, bottom = 0 }, + --}) + + --lineFrame:SetBackdropColor(1,0,1,1) + + lineFrame:SetScript("OnEnter", function(self) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnEnter") + if script then + script(self.iconFrame) + end + end + end) + lineFrame:SetScript("OnLeave", function(self) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnLeave") + if script then + script(self.iconFrame) + end + end + end) + lineFrame:RegisterForClicks("RightButtonUp", "LeftButtonUp") + lineFrame:SetScript("OnClick", function(self, button) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnClick") + if script then + script(self.iconFrame, button) + end + end + end) + + lineFrame:Hide(); + + + --Should we keep these frames in the questIdFrames? Currently it is also a child of the icon. + --Maybe the unload of the parent should just unload the children. + --For safety we check this here too. + --if (QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id] == nil) then + -- QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id] = {} + --end + --tinsert(QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id], lineFrame:GetName()); + + --Keep a total lineFrame count for names. + lineFrameCount = lineFrameCount + 1; + return lineFrame +end diff --git a/.history/Modules/FramePool/QuestieFramePool_20260222000626.lua b/.history/Modules/FramePool/QuestieFramePool_20260222000626.lua new file mode 100644 index 0000000..076c9bd --- /dev/null +++ b/.history/Modules/FramePool/QuestieFramePool_20260222000626.lua @@ -0,0 +1,398 @@ +---@class QuestieFramePool +local QuestieFramePool = QuestieLoader:CreateModule("QuestieFramePool") +------------------------- +--Import modules. +------------------------- +---@type QuestieQuest +local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest") +---@type QuestieMap +local QuestieMap = QuestieLoader:ImportModule("QuestieMap") +---@type MapIconTooltip +local MapIconTooltip = QuestieLoader:ImportModule("MapIconTooltip") +---@type QuestieLib +local QuestieLib = QuestieLoader:ImportModule("QuestieLib") +---@type l10n +local l10n = QuestieLoader:ImportModule("l10n") + +--- COMPATIBILITY --- +local C_Timer = QuestieCompat.C_Timer +local WorldMapFrame = QuestieCompat.WorldMapFrame + +local HBDPins = QuestieCompat.HBDPins or LibStub("HereBeDragonsQuestie-Pins-2.0") + +-- set pins parent to QuestieFrameGroup for easier compatibility with other addons +-- cant use this because it fucks with everything, but we gotta stick with HereBeDragonsQuestie anyway +HBDPins.MinimapGroup = CreateFrame("Frame", "QuestieFrameGroup", Minimap) +local WAYPOINT_COLOR = { 1, 0.72, 0, 0.5} + +local _QuestieFramePool = {} +local numberOfFrames = 0 + +local unusedFrames = {} +local usedFrames = {}; +local allFrames = {} + +StaticPopupDialogs["QUESTIE_CONFIRMHIDE"] = { + text = "", -- set before showing + questID = 0, -- set before showing + button1 = l10n("Yes"), + button2 = l10n("No"), + OnAccept = function() + QuestieQuest:HideQuest(StaticPopupDialogs["QUESTIE_CONFIRMHIDE"].questID) + end, + SetQuest = function(self, id) + self.questID = id + self.text = l10n("Are you sure you want to hide the quest '%s'?\nIf this quest isn't actually available, please report it to us!", QuestieLib:GetColoredQuestName(id, Questie.db.profile.enableTooltipsQuestLevel, false, true)) + + -- locale might not be loaded when this is first created (this does happen almost always) + self.button1 = l10n("Yes") + self.button2 = l10n("No") + end, + OnShow = function(self) + self:SetFrameStrata("TOOLTIP") + end, + timeout = 0, + whileDead = true, + hideOnEscape = true, + preferredIndex = 3 +} + +-- Global Functions -- +---@return IconFrame +function QuestieFramePool:GetFrame() + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:GetFrame]") + + ---@type IconFrame + local returnFrame = next(unusedFrames) + returnFrame = returnFrame and unusedFrames[returnFrame] + + if returnFrame and returnFrame.frameId and usedFrames[returnFrame.frameId] then + -- something went horribly wrong (desync bug?) don't use this frame since its already in use + Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieFramePool:GetFrame] Tried to reuse frame, but that frame is already in use. frameId:", returnFrame.frameId) + returnFrame = nil + end + if not returnFrame then + returnFrame = _QuestieFramePool:QuestieCreateFrame() + else + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:GetFrame] Reusing frame") + unusedFrames[returnFrame.frameId] = nil + end + if returnFrame ~= nil and returnFrame.hidden and returnFrame._show ~= nil and returnFrame._hide ~= nil then -- restore state to normal (toggle questie) + returnFrame.hidden = false + returnFrame.Show = returnFrame._show; + returnFrame.Hide = returnFrame._hide; + returnFrame._show = nil + returnFrame._hide = nil + end + returnFrame.FadeLogic = nil + returnFrame.faded = nil + returnFrame.miniMapIcon = nil + + returnFrame.data = nil + returnFrame.x = nil; + returnFrame.y = nil; + returnFrame.AreaID = nil; + returnFrame.UiMapID = nil + + if returnFrame.texture then + returnFrame.texture:SetVertexColor(1, 1, 1, 1) + end + returnFrame.loaded = true + returnFrame.shouldBeShowing = nil + returnFrame.hidden = nil + returnFrame._needsUnload = nil + + if returnFrame.BaseOnShow then + returnFrame:SetScript("OnShow", returnFrame.BaseOnShow) + end + + if returnFrame.BaseOnUpdate then + returnFrame.glowLogicTimer = C_Timer.NewTicker(1, returnFrame.BaseOnUpdate); + else + returnFrame:SetScript("OnUpdate", nil) + end + + if returnFrame.BaseOnHide then + returnFrame:SetScript("OnHide", returnFrame.BaseOnHide) + end + + usedFrames[returnFrame.frameId] = returnFrame + return returnFrame +end + +function QuestieFramePool:UpdateGlowConfig(mini, mode) + if mode then + for _, icon in pairs(usedFrames) do + if (((mini and icon.miniMapIcon) or not mini) and icon.glow) and icon.IsShown and icon:IsShown() then + icon:GetScript("OnShow")(icon) -- forces a glow update + end + end + else + for _, icon in pairs(usedFrames) do + if ((mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon)) and icon.glow then + icon.glow:Hide() + end + end + end +end + +function QuestieFramePool:UpdateColorConfig(mini, enable) + if enable then + for _, icon in pairs(usedFrames) do + if (mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon) then + local colors = {1, 1, 1} + if icon.data.IconColor ~= nil then + colors = icon.data.IconColor + end + icon.texture:SetVertexColor(colors[1], colors[2], colors[3], 1) + end + end + else + for _, icon in pairs(usedFrames) do + if (mini and icon.miniMapIcon) or (not mini and not icon.miniMapIcon) then + icon.texture:SetVertexColor(1, 1, 1, 1) + end + end + end +end + +function QuestieFramePool:RecycleFrame(frame) + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:RecycleFrame]") + usedFrames[frame.frameId] = nil + unusedFrames[frame.frameId] = frame +end + +function _QuestieFramePool:QuestieCreateFrame() + --Questie:Debug(Questie.DEBUG_SPAM, "[QuestieFramePool:QuestieCreateFrame]") + numberOfFrames = numberOfFrames + 1 + local newFrame = QuestieFramePool.Qframe:New(numberOfFrames, MapIconTooltip.Show) + + tinsert(allFrames, newFrame) + return newFrame +end + + +---@param iconFrame IconFrame @The parent frame for the current line. +---@param waypointTable table @A table containing waypoints {{X, Y}, ...} +---@param lineWidth number @Width of the line. +---@param color number[] @A table consisting of 4 variable {1, 1, 1, 1} RGB-Opacity +---@return LineFrame[] +function QuestieFramePool:CreateWaypoints(iconFrame, waypointTable, lineWidth, color, areaId) + local lineFrameList = {} + local lastPos + --Set defaults if needed. + local lWidth = lineWidth or 1.5; + local col = color or WAYPOINT_COLOR + + for _, waypointSubTable in pairs(waypointTable) do + lastPos = nil + for _, waypoint in pairs(waypointSubTable) do + if lastPos then + local lineFrame = QuestieFramePool:CreateLine(iconFrame, lastPos[1], lastPos[2], waypoint[1], waypoint[2], lWidth, col, areaId) + tinsert(lineFrameList, lineFrame); + end + lastPos = waypoint + end + end + return lineFrameList; +end + +local lineFrameCount = 1 + +---@param iconFrame IconFrame @The parent frame for the current line. +---@param startX number @A value between 0-100 +---@param startY number @A value between 0-100 +---@param endX number @A value between 0-100 +---@param endY number @A value between 0-100 +---@param lineWidth number @Width of the line. +---@param color number[] @A table consisting of 4 variable {1, 1, 1, 1} RGB-Opacity +---@return LineFrame +---@class LineFrame @A frame that contains the line used in waypoints. +function QuestieFramePool:CreateLine(iconFrame, startX, startY, endX, endY, lineWidth, color, areaId) + + --Create the framepool for lines if it does not already exist. + if not QuestieFramePool.Routes_Lines then + QuestieFramePool.Routes_Lines={} + end + --Names are not stricktly needed, but it is nice for debugging. + local frameName = "questieLineFrame".. lineFrameCount; + + --tremove default always picks the last element, however counting arrays is kinda bugged? So just get index 1 instead. + local lineFrame = tremove(QuestieFramePool.Routes_Lines, 1) or CreateFrame("Button", frameName, iconFrame); + if not lineFrame.frameId then + lineFrame.frameId = lineFrameCount; + end + + local canvas = WorldMapFrame:GetCanvas() + + local width = canvas:GetWidth(); + local height = canvas:GetHeight(); + + --Setting the parent is required to get the correct frame levels. + + lineFrame:SetParent(canvas) --This fixes the pan and zoom for lines + if QuestieCompat.Is335 then + lineFrame.CreateLine = QuestieCompat.CreateLine + else + lineFrame:SetFrameLevel(2015) -- This needs to be high, because of the regular WorldMapFrame.ScrollContainer + lineFrame:SetFrameStrata("FULLSCREEN") + end + + --How to identify what the frame actually contains, this is not used atm could easily be changed. + lineFrame.type = "line" + + --Include the line in the iconFrame. + if not iconFrame.data.lineFrames then + iconFrame.data.lineFrames = {}; + end + tinsert(iconFrame.data.lineFrames, lineFrame); + lineFrame.iconFrame = iconFrame; + lineFrame.data = iconFrame.data + lineFrame.x = (startX + endX) / 2 + lineFrame.y = (startY + endY) / 2 + lineFrame.AreaID = areaId or iconFrame.AreaID + lineFrame.texture = iconFrame.texture + + function lineFrame:Unload() + if not self.iconFrame then + return -- already unloaded + end + self:Hide(); + self.iconFrame = nil; + self.x = nil + self.y = nil + self.data = nil + self.texture = nil + self.AreaID = nil + HBDPins:RemoveWorldMapIcon(Questie, self) + tinsert(QuestieFramePool.Routes_Lines, self); + end + local line = lineFrame.line or lineFrame:CreateLine(); + lineFrame.line = line; + + line.dR = color[1]; + line.dG = color[2]; + line.dB = color[3]; + line.dA = color[4]; + line:SetColorTexture(color[1],color[2],color[3],color[4]); + + local lineBorder = lineFrame.lineBorder or lineFrame:CreateLine(); + lineFrame.lineBorder = lineBorder; + + lineBorder.dR = color[1]; + lineBorder.dG = color[2]; + lineBorder.dB = color[3]; + lineBorder.dA = color[4]; + lineBorder:SetColorTexture(0,0,0,color[4]/2); + + -- Set texture coordinates and anchors + --line:ClearAllPoints(); + + startX = startX * width / 100 + startY = startY * height / -100 -- We do by / -100 due to using the top left point + endX = endX * width / 100 + endY = endY * height / -100 + + width = abs(startX - endX) + lineWidth * 4 + height = abs(startY - endY) + lineWidth * 4 + + local framePosX = max(startX, endX) - lineWidth * 2 - width / 2 + local framePosY = min(startY, endY) + lineWidth * 2 + height / 2 + + lineFrame:SetHeight(height); + lineFrame:SetWidth(width); + lineFrame:SetPoint("TOPLEFT", canvas, "TOPLEFT", framePosX, framePosY) + + line:SetDrawLayer("OVERLAY", -5) + line:SetStartPoint("TOPLEFT", startX - framePosX, startY - framePosY) + line:SetEndPoint("TOPLEFT", endX - framePosX, endY - framePosY) + line:SetThickness(lineWidth); + + lineBorder:SetDrawLayer("OVERLAY", -6) + lineBorder:SetStartPoint("TOPLEFT", startX - framePosX, startY - framePosY) + lineBorder:SetEndPoint("TOPLEFT", endX - framePosX, endY - framePosY) + lineBorder:SetThickness(lineWidth+2); + + lineFrame:EnableMouse(true) + + --- This is needed because HBD will show the icons again after switching zones and stuff like that + function lineFrame:FakeHide() + if not self.hidden then + self.shouldBeShowing = self:IsShown(); + self._show = self.Show; + self.Show = function() + self.shouldBeShowing = true; + end + self:Hide(); + self._hide = self.Hide; + self.Hide = function() + self.shouldBeShowing = false; + end + self.hidden = true + end + end + + --- This is needed because HBD will show the icons again after switching zones and stuff like that + function lineFrame:FakeShow() + if self.hidden then + self.hidden = false + self.Show = self._show; + self.Hide = self._hide; + self._show = nil + self._hide = nil + if self.shouldBeShowing then + self:Show(); + end + end + end + + --lineFrame:SetBackdrop({ -- mouseover debugging + -- bgFile = "Interface/Tooltips/UI-Tooltip-Background", + -- edgeFile = nil, + -- edgeSize = 0, + -- insets = { left = 0, right = 0, top = 0, bottom = 0 }, + --}) + + --lineFrame:SetBackdropColor(1,0,1,1) + + lineFrame:SetScript("OnEnter", function(self) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnEnter") + if script then + script(self.iconFrame) + end + end + end) + lineFrame:SetScript("OnLeave", function(self) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnLeave") + if script then + script(self.iconFrame) + end + end + end) + lineFrame:RegisterForClicks("RightButtonUp", "LeftButtonUp") + lineFrame:SetScript("OnClick", function(self, button) + if self and self.iconFrame then + local script = self.iconFrame:GetScript("OnClick") + if script then + script(self.iconFrame, button) + end + end + end) + + lineFrame:Hide(); + + + --Should we keep these frames in the questIdFrames? Currently it is also a child of the icon. + --Maybe the unload of the parent should just unload the children. + --For safety we check this here too. + --if (QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id] == nil) then + -- QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id] = {} + --end + --tinsert(QuestieMap.questIdFrames[lineFrame.iconFrame.data.Id], lineFrame:GetName()); + + --Keep a total lineFrame count for names. + lineFrameCount = lineFrameCount + 1; + return lineFrame +end diff --git a/.history/Modules/Quest/QuestieQuest_20260222000236.lua b/.history/Modules/Quest/QuestieQuest_20260222000236.lua new file mode 100644 index 0000000..399a19a --- /dev/null +++ b/.history/Modules/Quest/QuestieQuest_20260222000236.lua @@ -0,0 +1,1728 @@ +--- COMPATIBILITY --- +local IsQuestFlaggedCompleted = QuestieCompat.IsQuestFlaggedCompleted or C_QuestLog.IsQuestFlaggedCompleted + +---@class QuestieQuest +local QuestieQuest = QuestieLoader:CreateModule("QuestieQuest") +---@type QuestieQuestPrivate +QuestieQuest.private = QuestieQuest.private or {} +local _QuestieQuest = QuestieQuest.private +------------------------- +--Import modules. +------------------------- +---@type QuestieProfessions +local QuestieProfessions = QuestieLoader:ImportModule("QuestieProfessions") +---@type QuestieReputation +local QuestieReputation = QuestieLoader:ImportModule("QuestieReputation") +---@type QuestieTooltips +local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") +---@type QuestieTracker +local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker") +---@type QuestieDBMIntegration +local QuestieDBMIntegration = QuestieLoader:ImportModule("QuestieDBMIntegration") +---@type QuestieMap +local QuestieMap = QuestieLoader:ImportModule("QuestieMap") +---@type QuestieLib +local QuestieLib = QuestieLoader:ImportModule("QuestieLib") +---@type QuestiePlayer +local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer") +---@type TaskQueue +local TaskQueue = QuestieLoader:ImportModule("TaskQueue") +---@type QuestieDB +local QuestieDB = QuestieLoader:ImportModule("QuestieDB") +---@type ZoneDB +local ZoneDB = QuestieLoader:ImportModule("ZoneDB") +---@type QuestieCombatQueue +local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue") +---@type QuestieAnnounce +local QuestieAnnounce = QuestieLoader:ImportModule("QuestieAnnounce") +---@type QuestieMenu +local QuestieMenu = QuestieLoader:ImportModule("QuestieMenu") +---@type l10n +local l10n = QuestieLoader:ImportModule("l10n") +---@type QuestLogCache +local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") +---@type ThreadLib +local ThreadLib = QuestieLoader:ImportModule("ThreadLib") +---@type AvailableQuests +local AvailableQuests = QuestieLoader:ImportModule("AvailableQuests") + +--- COMPATIBILITY --- +local C_Timer = QuestieCompat.C_Timer +local GetQuestsCompleted = QuestieCompat.GetQuestsCompleted +local xpcall = QuestieCompat.xpcall + +--We should really try and squeeze out all the performance we can, especially in this. +local tostring = tostring; +local tinsert = table.insert; +local pairs = pairs; +local ipairs = ipairs; +local yield = coroutine.yield +local NewThread = ThreadLib.ThreadSimple + +local NOP_FUNCTION = function() +end +local ERR_FUNCTION = function(err) + print(err) + print(debugstack()) +end + +-- forward declaration +local _UnloadAlreadySpawnedIcons +local _RegisterObjectiveTooltips, _DetermineIconsToDraw, _GetIconsSortedByDistance +local _DrawObjectiveIcons, _DrawObjectiveWaypoints + +local HBD = QuestieCompat.HBD or LibStub("HereBeDragonsQuestie-2.0") + +function QuestieQuest:Initialize() + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest]: Getting all completed quests") + Questie.db.char.complete = GetQuestsCompleted() + + QuestieProfessions:Update() + QuestieReputation:Update(true) +end + +---@param category AutoBlacklistString +function QuestieQuest.ResetAutoblacklistCategory(category) + Questie:Debug(Questie.DEBUG_SPAM, "[QuestieQuest]: Resetting autoblacklist category", category) + for questId, questCategory in pairs(QuestieDB.autoBlacklist) do + if questCategory == category then + QuestieDB.autoBlacklist[questId] = nil + end + end +end + +function QuestieQuest:ToggleNotes(showIcons) + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:ToggleNotes] showIcons:", showIcons) + QuestieQuest:GetAllQuestIds() -- add notes that weren't added from previous hidden state + + if showIcons then + _QuestieQuest:ShowQuestIcons() + _QuestieQuest:ShowManualIcons() + else + _QuestieQuest:HideQuestIcons() + _QuestieQuest:HideManualIcons() + end +end + +function _QuestieQuest:ShowQuestIcons() + local trackerHiddenQuests = Questie.db.char.TrackerHiddenQuests + for questId, frameList in pairs(QuestieMap.questIdFrames) do + if (not trackerHiddenQuests) or (not trackerHiddenQuests[questId]) then -- Skip quests which are completely hidden from the Tracker menu + for _, frameName in pairs(frameList) do -- this may seem a bit expensive, but its actually really fast due to the order things are checked + ---@type IconFrame + local icon = _G[frameName]; + if not icon.data then + error("Desync! Icon has not been removed correctly, but has already been reset. Skipping frame \"" .. + frameName .. "\" for quest " .. questId) + else + local objectiveString = tostring(questId) .. " " .. tostring(icon.data.ObjectiveIndex) + if (not Questie.db.char.TrackerHiddenObjectives) or (not Questie.db.char.TrackerHiddenObjectives[objectiveString]) then + if icon ~= nil and icon.hidden and (not icon:ShouldBeHidden()) then + icon:FakeShow() + + if icon.data.lineFrames then + for _, lineIcon in pairs(icon.data.lineFrames) do + lineIcon:FakeShow() + end + end + end + if (icon.data.QuestData.FadeIcons or (icon.data.ObjectiveData and icon.data.ObjectiveData.FadeIcons)) and icon.data.Type ~= "complete" then + icon:FadeOut() + else + icon:FadeIn() + end + end + end + end + end + end +end + +function _QuestieQuest:ShowManualIcons() + for _, frameList in pairs(QuestieMap.manualFrames) do + for _, frameName in pairs(frameList) do + local icon = _G[frameName]; + if icon ~= nil and icon.hidden and (not icon:ShouldBeHidden()) then -- check for function to make sure its a frame + icon:FakeShow() + end + end + end +end + +function _QuestieQuest:HideQuestIcons() + for _, frameList in pairs(QuestieMap.questIdFrames) do + for _, frameName in pairs(frameList) do -- this may seem a bit expensive, but its actually really fast due to the order things are checked + local icon = _G[frameName]; + if icon ~= nil and (not icon.hidden) and icon:ShouldBeHidden() then -- check for function to make sure its a frame + -- Hides Objective Icons + icon:FakeHide() + + -- Hides Objective Tooltips + QuestieTooltips:RemoveQuest(icon.data.Id) + + if icon.data.lineFrames then + for _, lineIcon in pairs(icon.data.lineFrames) do + lineIcon:FakeHide() + end + end + end + if (icon.data.QuestData.FadeIcons or (icon.data.ObjectiveData and icon.data.ObjectiveData.FadeIcons)) and icon.data.Type ~= "complete" then + icon:FadeOut() + else + icon:FadeIn() + end + end + end +end + +function _QuestieQuest:HideManualIcons() + for _, frameList in pairs(QuestieMap.manualFrames) do + for _, frameName in pairs(frameList) do + local icon = _G[frameName]; + if icon ~= nil and (not icon.hidden) and icon:ShouldBeHidden() then -- check for function to make sure its a frame + icon:FakeHide() + end + end + end +end + +function QuestieQuest:ClearAllNotes() + for questId in pairs(QuestiePlayer.currentQuestlog) do + local quest = QuestieDB.GetQuest(questId) + + if not quest then + return + end + + for _, s in pairs(quest.Objectives) do + s.AlreadySpawned = {} + end + + if next(quest.SpecialObjectives) then + for _, s in pairs(quest.SpecialObjectives) do + s.AlreadySpawned = {} + end + end + end + + for _, frameList in pairs(QuestieMap.questIdFrames) do + for _, frameName in pairs(frameList) do + local icon = _G[frameName] + if icon and icon.Unload then + icon:Unload() + end + end + end + + QuestieMap.questIdFrames = {} +end + +function QuestieQuest:ClearAllToolTips() + for questId in pairs(QuestiePlayer.currentQuestlog) do + local quest = QuestieDB.GetQuest(questId) + + if not quest then + return + end + + if quest.Objectives then + for _, objective in pairs(quest.Objectives) do + if objective.hasRegisteredTooltips then + objective.hasRegisteredTooltips = false + end + + if objective.registeredItemTooltips then + objective.registeredItemTooltips = false + end + end + end + + if quest.ObjectiveData then + for _, objective in pairs(quest.ObjectiveData) do + if objective.hasRegisteredTooltips then + objective.hasRegisteredTooltips = false + end + + if objective.registeredItemTooltips then + objective.registeredItemTooltips = false + end + end + end + + if next(quest.SpecialObjectives) then + for _, objective in pairs(quest.SpecialObjectives) do + if objective.hasRegisteredTooltips then + objective.hasRegisteredTooltips = false + end + + if objective.registeredItemTooltips then + objective.registeredItemTooltips = false + end + end + end + end + + QuestieTooltips.lookupByKey = {} + QuestieTooltips.lookupKeyByQuestId = {} +end + +-- This is only needed for SmoothReset(), normally special objectives don't need to update +---@param questId number +local function _UpdateSpecials(questId) + local quest = QuestieDB.GetQuest(questId) + if quest and next(quest.SpecialObjectives) then + for _, objective in pairs(quest.SpecialObjectives) do + local result, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, 0, objective, + true) + if not result then + Questie:Error("[QuestieQuest]: [SpecialObjectives] " .. + l10n("There was an error populating objectives for %s %s %s %s", quest.name or "No quest name", + quest.Id or "No quest id", 0 or "No objective", err or "No error")); + end + end + end +end + +function QuestieQuest:SmoothReset() + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:SmoothReset]") + if QuestieQuest._isResetting then + QuestieQuest._resetAgain = true + return + end + QuestieQuest._isResetting = true + QuestieQuest._resetNeedsAvailables = false + + -- bit of a hack (there has to be a better way to do logic like this + QuestieDBMIntegration:ClearAll() + local stepTable = { + function() + -- Wait until game cache has quest log okay. + return QuestLogCache.TestGameCache() + end, + function() + return #QuestieMap._mapDrawQueue == 0 and + #QuestieMap._minimapDrawQueue == 0 -- wait until draw queue is finished + end, + function() + QuestieQuest:ClearAllNotes() + QuestieQuest:ClearAllToolTips() + return true + end, + function() + QuestieMenu:OnLogin(true) -- remove icons + return true + end, + function() + return #QuestieMap._mapDrawQueue == 0 and + #QuestieMap._minimapDrawQueue == 0 -- wait until draw queue is finished + end, + function() + -- reset quest log + QuestiePlayer.currentQuestlog = {} + + --- reset the blacklist + QuestieDB.autoBlacklist = {} + + -- make sure complete db is correct + Questie.db.char.complete = GetQuestsCompleted() + QuestieProfessions:Update() + QuestieReputation:Update(true) + + -- populate QuestiePlayer.currentQuestlog + QuestieQuest:GetAllQuestIdsNoObjectives() + QuestieQuest._nextRestQuest = next(QuestiePlayer.currentQuestlog) + return true + end, + function() + QuestieMenu:OnLogin() + return true + end, + function() + QuestieQuest._resetNeedsAvailables = true + AvailableQuests.CalculateAndDrawAll(function() QuestieQuest._resetNeedsAvailables = false end) + return true + end, + function() + for _ = 1, 64 do + if QuestieQuest._nextRestQuest then + QuestieQuest:UpdateQuest(QuestieQuest._nextRestQuest) + _UpdateSpecials(QuestieQuest._nextRestQuest) + QuestieQuest._nextRestQuest = next(QuestiePlayer.currentQuestlog, QuestieQuest._nextRestQuest) + else + QuestieCombatQueue:Queue(function() + C_Timer.After(2.0, function() + QuestieTracker:Update() + end) + end) + break + end + end + return not QuestieQuest._nextRestQuest + end, + function() + return (not QuestieQuest._resetNeedsAvailables) and #QuestieMap._mapDrawQueue == 0 and + #QuestieMap._minimapDrawQueue == 0 + end, + function() + QuestieQuest._isResetting = nil + if QuestieQuest._resetAgain then + QuestieQuest._resetAgain = nil + QuestieQuest:SmoothReset() + end + return true + end + } + local step = 1 + local ticker + ticker = C_Timer.NewTicker(0.01, function() + if stepTable[step]() then + step = step + 1 + if not stepTable[step] then + ticker:Cancel() + end + end + if QuestieQuest._resetAgain and not QuestieQuest._resetNeedsAvailables then -- we can stop the current reset + ticker:Cancel() + QuestieQuest._resetAgain = nil + QuestieQuest._isResetting = nil + QuestieQuest:SmoothReset() + end + end) +end + +---@param questId number +---@return boolean +function QuestieQuest:ShouldShowQuestNotes(questId) + if not Questie.db.profile.hideUntrackedQuestsMapIcons then + return true + end + + local autoWatch = Questie.db.profile.autoTrackQuests + local trackedAuto = autoWatch and + (not Questie.db.char.AutoUntrackedQuests or not Questie.db.char.AutoUntrackedQuests[questId]) + local trackedManual = not autoWatch and (Questie.db.char.TrackedQuests and Questie.db.char.TrackedQuests[questId]) + return trackedAuto or trackedManual +end + +function QuestieQuest:HideQuest(id) + Questie.db.char.hidden[id] = true + QuestieMap:UnloadQuestFrames(id) + QuestieTooltips:RemoveQuest(id) +end + +function QuestieQuest:UnhideQuest(id) + Questie.db.char.hidden[id] = nil + AvailableQuests.CalculateAndDrawAll() +end + +local allianceTournamentMarkerQuests = { [13684] = true, [13685] = true, [13688] = true, [13689] = true, [13690] = true, + [13593] = true, [13703] = true, [13704] = true, [13705] = true, [13706] = true } +local hordeTournamentMarkerQuests = { [13691] = true, [13693] = true, [13694] = true, [13695] = true, [13696] = true, + [13707] = true, [13708] = true, [13709] = true, [13710] = true, [13711] = true } + +---@param questId number +function QuestieQuest:AcceptQuest(questId) + local quest = QuestieDB.GetQuest(questId) + + if quest then + local complete = QuestieDB.IsComplete(questId) + -- If any of these flags exsist then this quest has already once been accepted and is probobly in a failed state + if (quest.WasComplete or quest.isComplete or complete == 0 or complete == -1) and (QuestiePlayer.currentQuestlog[questId]) then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId, + " Warning: This quest was once accepted and needs to be reset.") + + -- Reset quest log + QuestiePlayer.currentQuestlog[questId] = nil + + -- Reset quest objectives + quest.Objectives = {} + + -- Reset quest flags + quest.WasComplete = nil + quest.isComplete = nil + + -- Reset tooltips + QuestieTooltips:RemoveQuest(questId) + end + + if not QuestiePlayer.currentQuestlog[questId] then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId) + + QuestiePlayer.currentQuestlog[questId] = quest + + if allianceTournamentMarkerQuests[questId] then + Questie.db.char.complete[13686] = true -- Alliance Tournament Eligibility Marker + elseif hordeTournamentMarkerQuests[questId] then + Questie.db.char.complete[13687] = true -- Horde Tournament Eligibility Marker + end + + TaskQueue:Queue( + -- Get all the Frames for the quest and unload them, the available quest icon for example. + function() QuestieMap:UnloadQuestFrames(questId) end, + -- Make sure there isn't any lingering tooltip data hanging around in the quest table. + function() QuestieTooltips:RemoveQuest(questId) end, + function() + -- Re-accepted quest can be collapsed. Expand it. Especially dailies. + if Questie.db.char.collapsedQuests then + Questie.db.char.collapsedQuests[questId] = nil + end + -- Re-accepted quest can be untracked. Clear it. Especially timed quests. + if Questie.db.char.AutoUntrackedQuests[questId] then + Questie.db.char.AutoUntrackedQuests[questId] = nil + end + end, + function() QuestieQuest:PopulateQuestLogInfo(quest) end, + function() + -- This needs to happen after QuestieQuest:PopulateQuestLogInfo because that is the place where quest.Objectives is generated + Questie:SendMessage("QC_ID_BROADCAST_QUEST_UPDATE", questId) + end, + function() QuestieQuest:PopulateObjectiveNotes(quest) end, + function() AvailableQuests.CalculateAndDrawAll() end, + function() + QuestieCombatQueue:Queue(function() + QuestieTracker:Update() + end) + end + ) + else + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId, + " Warning: Quest already exists, not adding") + end + end +end + +local allianceChampionMarkerQuests = { [13699] = true, [13713] = true, [13723] = true, [13724] = true, [13725] = true } +local hordeChampionMarkerQuests = { [13726] = true, [13727] = true, [13728] = true, [13729] = true, [13731] = true } + +---@param questId number +function QuestieQuest:CompleteQuest(questId) + -- Skip quests which are turn in only and are not added to the quest log in the first place + local questLogEntry = QuestiePlayer.currentQuestlog[questId] + if questLogEntry then + -- Only reset flags if the entry is a table (not a legacy number) + if type(questLogEntry) == "table" then + questLogEntry.WasComplete = nil + questLogEntry.isComplete = nil + end + QuestiePlayer.currentQuestlog[questId] = nil; + end + + -- Only quests that are daily quests or aren't repeatable should be marked complete, + -- otherwise objectives for repeatable quests won't track correctly - #1433 + if QuestieCompat.Is335 then + QuestieCompat.SetQuestComplete(questId) + else + Questie.db.char.complete[questId] = (not QuestieDB.IsRepeatable(questId)) or QuestieDB.IsDailyQuest(questId) or + QuestieDB.IsWeeklyQuest(questId); + end + + if allianceChampionMarkerQuests[questId] then + Questie.db.char.complete[13700] = true -- Alliance Champion Marker + Questie.db.char.complete[13686] = nil -- Alliance Tournament Eligibility Marker + elseif hordeChampionMarkerQuests[questId] then + Questie.db.char.complete[13701] = true -- Horde Champion Marker + Questie.db.char.complete[13687] = nil -- Horde Tournament Eligibility Marker + end + QuestieMap:UnloadQuestFrames(questId) + + -- Clear the pending-complete guard now that frames are unloaded + if QuestiePlayer.pendingCompleteQuestIds then + QuestiePlayer.pendingCompleteQuestIds[questId] = nil + end + + if (QuestieMap.questIdFrames[questId]) then + Questie:Error("Just removed all frames but the framelist seems to still be there!", questId) + end + + -- Delayed verification to ensure all objective icons are removed + -- This handles race conditions where AvailableQuests might redraw icons or UnloadQuestFrames misses some + C_Timer.After(0.5, function() + if QuestieMap.questIdFrames[questId] then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:CompleteQuest] Lingering frames detected for quest:", + questId, "- forcing cleanup") + QuestieMap:UnloadQuestFrames(questId) + end + end) + + QuestieTooltips:RemoveQuest(questId) + QuestieTracker:RemoveQuest(questId) + QuestieCombatQueue:Queue(function() + QuestieTracker:Update() + end) + + -- TODO: Should this be done first? Because CalculateAndDrawAll looks at QuestieMap.questIdFrames[QuestId] to add available + AvailableQuests.CalculateAndDrawAll() + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Completed Quest:", questId) +end + +---@param questId number +function QuestieQuest:AbandonedQuest(questId) + -- NOTE: Some servers remove quests without firing QUEST_REMOVED reliably. + -- Always cleanup frames/tracker even if the quest isn't currently in QuestiePlayer.currentQuestlog. + if QuestiePlayer.currentQuestlog then + QuestiePlayer.currentQuestlog[questId] = nil + end + + QuestieMap:UnloadQuestFrames(questId) + + local quest = QuestieDB.GetQuest(questId) + if quest then + -- Reset quest objectives + quest.Objectives = {} + + -- Reset quest flags + quest.WasComplete = nil + quest.isComplete = nil + + if allianceTournamentMarkerQuests[questId] then + Questie.db.char.complete[13686] = nil -- Alliance Tournament Eligibility Marker + elseif hordeTournamentMarkerQuests[questId] then + Questie.db.char.complete[13687] = nil -- Horde Tournament Eligibility Marker + end + + local childQuests = QuestieDB.QueryQuestSingle(questId, "childQuests") + if childQuests then + for _, childQuestId in pairs(childQuests) do + Questie.db.char.complete[childQuestId] = nil + QuestLogCache.RemoveQuest(childQuestId) + end + end + end + + AvailableQuests.UnloadUndoable() + + QuestieTracker:RemoveQuest(questId) + QuestieTooltips:RemoveQuest(questId) + QuestieCombatQueue:Queue(function() + QuestieTracker:Update() + end) + + AvailableQuests.CalculateAndDrawAll() + + -- Delayed verification to ensure all objective icons are removed + -- This handles race conditions where QuestieQuest:UpdateQuest might redraw icons + -- just as we are abandoning the quest + C_Timer.After(0.5, function() + if QuestieMap.questIdFrames[questId] then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:AbandonedQuest] Lingering frames detected for quest:", + questId, "- forcing cleanup") + QuestieMap:UnloadQuestFrames(questId) + end + end) + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Abandoned Quest:", questId) +end + +---@param questId number +function QuestieQuest:UpdateQuest(questId) + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest]", questId) + + ---@type Quest + local quest = QuestieDB.GetQuest(questId) + + local sourceItemId = (quest and tonumber(quest.sourceItemId)) or 0 + + if quest and (not Questie.db.char.complete[questId] or QuestiePlayer.currentQuestlog[questId]) then + -- Skip this update if the quest is mid-completion to avoid redrawing objective pins + -- that CompleteQuest is about to remove via UnloadQuestFrames. + if QuestiePlayer.pendingCompleteQuestIds and QuestiePlayer.pendingCompleteQuestIds[questId] then + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Skipping - quest is pending completion:", questId) + return + end + QuestieQuest:PopulateQuestLogInfo(quest) + + if QuestieQuest:ShouldShowQuestNotes(questId) then + QuestieQuest:UpdateObjectiveNotes(quest) + else + QuestieTooltips:RemoveQuest(questId) + end + + local isComplete = QuestieDB.IsComplete(questId) + + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] QuestDB:IsComplete() flag is: " .. isComplete) + + if isComplete == 1 then + -- Quest is complete + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Quest is: Complete!") + + QuestieMap:UnloadQuestFrames(questId) + QuestieQuest:AddFinisher(quest) + quest.WasComplete = true + elseif isComplete == -1 then + -- Failed quests should be shown as available again + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Quest has: Failed!") + + QuestieMap:UnloadQuestFrames(questId) + QuestieTooltips:RemoveQuest(questId) + AvailableQuests.DrawAvailableQuest(quest) + + -- Reset any collapsed quest flags + if Questie.db.char.collapsedQuests then + Questie.db.char.collapsedQuests[questId] = nil + end + elseif isComplete == 0 then + -- Quest was somehow reset back to incomplete after being completed (quest.WasComplete == true). + -- The "or" check looks for a sourceItemId then checks to see if it's NOT in the players bag. + -- Player destroyed quest items? Or some other quest mechanic removed the needed quest item. + -- Check if all objectives are already complete before treating a missing source item as a reset. + -- Some quests use consumable key items (e.g. Cold Iron Key for quest 12843). After using the key + -- the item leaves the bag, making CheckQuestSourceItem return false. Without this guard, Questie + -- would reset the quest and draw the key-drop NPC on the map even though all objectives are done. + local allObjectivesComplete = false + if quest.Objectives and #quest.Objectives > 0 then + local doneCount = 0 + for i = 1, #quest.Objectives do + if quest.Objectives[i] and quest.Objectives[i].Completed == true then + doneCount = doneCount + 1 + end + end + allObjectivesComplete = (doneCount == #quest.Objectives) + end + + if quest and not allObjectivesComplete and (quest.WasComplete or (sourceItemId > 0 and QuestieQuest:CheckQuestSourceItem(questId) == false)) then + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest:UpdateQuest] Quest was once complete or Quest Item(s) were removed. Resetting quest.") + + -- Reset quest objectives + quest.Objectives = {} + + -- Reset quest flags + quest.WasComplete = nil + quest.isComplete = nil + + -- Reset tooltips + QuestieTooltips:RemoveQuest(questId) + + QuestieQuest:CheckQuestSourceItem(questId, true) + QuestieMap:UnloadQuestFrames(questId) + + -- Reset any collapsed quest flags + if Questie.db.char.collapsedQuests then + Questie.db.char.collapsedQuests[questId] = nil + end + + QuestieQuest:PopulateQuestLogInfo(quest) + QuestieQuest:PopulateObjectiveNotes(quest) + AvailableQuests.CalculateAndDrawAll() + else + -- Sometimes objective(s) are all complete but the quest doesn't get flagged as "1". So far the only + -- quests I've found that does this are quests involving an item(s). Checks all objective(s) and if they + -- are all complete, simulate a "Complete Quest" so the quest finisher appears on the map. + if quest.Objectives and #quest.Objectives > 0 then + local numCompleteObjectives = 0 + + for i = 1, #quest.Objectives do + if quest.Objectives[i] and quest.Objectives[i].Completed and quest.Objectives[i].Completed == true then + numCompleteObjectives = numCompleteObjectives + 1 + end + end + + if numCompleteObjectives == #quest.Objectives then + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest:UpdateQuest] All Quest Objective(s) are Complete! Manually setting quest to Complete!") + QuestieMap:UnloadQuestFrames(questId) + QuestieQuest:AddFinisher(quest) + quest.WasComplete = true + quest.isComplete = true + else + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest:UpdateQuest] Quest Objective Status is: " .. + numCompleteObjectives .. ", out of: " .. #quest.Objectives .. ". No updates required.") + end + end + end + end + + Questie:SendMessage("QC_ID_BROADCAST_QUEST_UPDATE", questId) + end +end + +---@param questId number +function QuestieQuest:SetObjectivesDirty(questId) + local quest = QuestieDB.GetQuest(questId) + + if quest then + for _, objective in pairs(quest.Objectives) do + objective.isUpdated = false + end + end +end + +--Run this if you want to update the entire table +function QuestieQuest:GetAllQuestIds() + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] Getting all quests") + + QuestiePlayer.currentQuestlog = {} + + for questId, data in pairs(QuestLogCache.questLog_DO_NOT_MODIFY) do -- DO NOT MODIFY THE RETURNED TABLE + local quest = QuestieDB.GetQuest(questId) + + if not quest then + if not Questie._sessionWarnings[questId] then + if not Questie.IsSoD then + Questie:Error(l10n( + "The quest %s is missing from Questie's database. Please report this on GitHub or Discord!", + tostring(questId))) + end + Questie._sessionWarnings[questId] = true + end + + QuestiePlayer.currentQuestlog[questId] = questId -- legacy behavior + else + local complete = QuestieDB.IsComplete(questId) + + QuestiePlayer.currentQuestlog[questId] = quest + quest.LocalizedName = data.title + + if complete == -1 then + QuestieQuest:UpdateQuest(questId) + else + -- Only draw the source item objective when the quest is not yet complete. + -- If the quest is complete (complete == 1), the source item was consumed during + -- the quest (e.g. Cold Iron Key for quest 12843) and should not draw its drop NPC. + if complete == 1 then + -- Mark the quest object as complete so QuestieArrow collects finisher spawns + -- instead of objective spawns. Without this, the arrow falls through to the + -- objective collection path and picks up any stale fake item objectives. + quest.isComplete = true + quest.WasComplete = true + else + QuestieQuest:CheckQuestSourceItem(questId, true) + end + QuestieQuest:PopulateQuestLogInfo(quest) + + if QuestieQuest:ShouldShowQuestNotes(questId) then + QuestieQuest:PopulateObjectiveNotes(quest) + else + QuestieTooltips:RemoveQuest(questId) + end + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Adding the quest", questId, + QuestiePlayer.currentQuestlog[questId]) + end + end + + QuestieCombatQueue:Queue(function() + QuestieTracker:Update() + end) +end + +-- This checks and manually adds quest item tooltips for sourceItems +local function _AddSourceItemObjective(quest) + local sourceItemId = tonumber(quest.sourceItemId) or 0 + if sourceItemId <= 0 then + return + end + + local questObjectives = QuestieDB.QueryQuestSingle(quest.Id, "objectives") + local itemObjectives = questObjectives and questObjectives[3] + + -- If sourceItemId is already part of an item objective, do nothing + if itemObjectives then + for _, itemObjectiveIndex in pairs(itemObjectives) do + for _, itemObjectiveId in pairs(itemObjectiveIndex) do + if itemObjectiveId == sourceItemId then + Questie:Debug(Questie.DEBUG_INFO, + "[QuestieQuest:_AddSourceItemObjective] This item is already part of a quest objective.") + return + end + end + end + end + + local itemName = QuestieDB.QueryItemSingle(sourceItemId, "name") + if not itemName then + return + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_AddSourceItemObjective] Adding Source Item Id for:", sourceItemId) + + local fakeObjective = { + Id = quest.Id, + IsSourceItem = true, + QuestData = quest, + Index = 1, + Needed = 1, + Collected = 1, + text = itemName, + Description = itemName + } + + QuestieTooltips:RegisterObjectiveTooltip(quest.Id, "i_" .. sourceItemId, fakeObjective); +end + +-- This checks and manually adds quest item tooltips for SpellItems +local function _AddSpellItemObjective(quest) + if not quest.SpellItemId then + return + end + + local questObjectives = QuestieDB.QueryQuestSingle(quest.Id, "objectives") + local spellObjectives = questObjectives and questObjectives[6] + if not spellObjectives then + return + end + + local depthIndex = 1 -- TODO: What is better for this? + local needed = (quest.Objectives and quest.Objectives[depthIndex] and quest.Objectives[depthIndex].Needed) or 0 + local collected = (quest.Objectives and quest.Objectives[depthIndex] and quest.Objectives[depthIndex].Collected) or 0 + local desc = (quest.Objectives and quest.Objectives[depthIndex] and quest.Objectives[depthIndex].Description) or "" + + local fakeObjective = { + Id = quest.Id, + IsSourceItem = true, + QuestData = quest, + Index = 1, + Needed = needed, + Collected = collected, + text = nil, + Description = desc, + } + + QuestieTooltips:RegisterObjectiveTooltip(quest.Id, "i_" .. quest.SpellItemId, fakeObjective); +end + + +-- This checks and manually adds quest item tooltips for requiredSourceItems +local function _AddRequiredSourceItemObjective(quest) + if not quest.requiredSourceItems then + return + end + + local questObjectives = QuestieDB.QueryQuestSingle(quest.Id, "objectives") + local itemObjectives = questObjectives and questObjectives[3] + + for index, requiredSourceItemId in pairs(quest.requiredSourceItems) do + local alreadyInObjectives = false + + if itemObjectives then + for _, itemObjectiveIndex in pairs(itemObjectives) do + for _, itemObjectiveId in pairs(itemObjectiveIndex) do + if itemObjectiveId == requiredSourceItemId or quest.sourceItemId == requiredSourceItemId then + Questie:Debug(Questie.DEBUG_INFO, + "[QuestieQuest:_AddRequiredSourceItemObjective] This item is already part of a quest objective.") + alreadyInObjectives = true + break + end + end + if alreadyInObjectives then + break + end + end + end + + if not alreadyInObjectives then + local itemName = QuestieDB.QueryItemSingle(requiredSourceItemId, "name") + if itemName then + Questie:Debug(Questie.DEBUG_INFO, + "[QuestieQuest:_AddRequiredSourceItemObjective] Adding Source Item Id for:", requiredSourceItemId) + + local fakeObjective = { + Id = quest.Id, + IsRequiredSourceItem = true, + QuestData = quest, + Index = index, + text = itemName, + Description = itemName + } + + QuestieTooltips:RegisterObjectiveTooltip(quest.Id, "i_" .. requiredSourceItemId, fakeObjective); + end + end + end +end + + + +function QuestieQuest:GetAllQuestIdsNoObjectives() + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] Getting all quests without objectives") + QuestiePlayer.currentQuestlog = {} + + for questId, data in pairs(QuestLogCache.questLog_DO_NOT_MODIFY) do -- DO NOT MODIFY THE RETURNED TABLE + local quest = QuestieDB.GetQuest(questId) + + if not quest then + if not Questie._sessionWarnings[questId] then + if not Questie.IsSoD then + Questie:Error(l10n( + "The quest %s is missing from Questie's database. Please report this on GitHub or Discord!", + tostring(questId))) + end + Questie._sessionWarnings[questId] = true + end + + QuestiePlayer.currentQuestlog[questId] = questId + else + QuestiePlayer.currentQuestlog[questId] = quest + quest.LocalizedName = data.title + _AddSourceItemObjective(quest) + _AddRequiredSourceItemObjective(quest) + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Adding the quest", questId, + QuestiePlayer.currentQuestlog[questId]) + end + end +end + +-- iterate all notes, update / remove as needed +---@param quest Quest +function QuestieQuest:UpdateObjectiveNotes(quest) + if (not QuestiePlayer.currentQuestlog) or (not QuestiePlayer.currentQuestlog[quest.Id]) then + return + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] UpdateObjectiveNotes:", quest.Id) + for objectiveIndex, objective in pairs(quest.Objectives) do + local result, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, objectiveIndex, + objective, false) + if (not result) then + Questie:Debug(Questie.DEBUG_ELEVATED, "[QuestieQuest] There was an error populating objectives for", + quest.name, quest.Id, objectiveIndex, err) + end + end + + if next(quest.SpecialObjectives) then + for _, objective in pairs(quest.SpecialObjectives) do + local result, err = xpcall(QuestieQuest.PopulateObjective, ERR_FUNCTION, QuestieQuest, quest, 0, objective, + true) + if not result then + Questie:Error("[QuestieQuest]: [SpecialObjectives] " .. + l10n("There was an error populating objectives for %s %s %s %s", quest.name or "No quest name", + quest.Id or "No quest id", 0 or "No objective", err or "No error")); + end + end + end +end + +-- This function is used to check the players bags for an item that matches quest.sourceItemId. +-- A good example for this edge case is [18] The Price of Shoes (118) where upon acceptance, Verner's Note (1283) is given +-- to the player and the Quest is immediately flagged as Complete. If the note is destroyed then a slightly modified version +-- of QuestieDB.IsComplete() that uses this function, returns zero allowing the quest updates to properly set the quests state. +---@param questId number @QuestID +---@param makeObjective boolean @If set to true, then this will create an incomplete objective for the missing quest item +---@return boolean @Returns true if quest.sourceItemId matches an item in a players bag +function QuestieQuest:CheckQuestSourceItem(questId, makeObjective) + local quest = QuestieDB.GetQuest(questId) + local sourceItem = true + + -- Ascension/custom quests may omit sourceItemId (nil). Normalize to 0. + local sourceItemId = (quest and tonumber(quest.sourceItemId)) or 0 + + if quest and sourceItemId > 0 then + for bag = -2, 4 do + local numSlots = QuestieCompat.GetContainerNumSlots(bag) or 0 + for slot = 1, numSlots do + local itemId = select(10, QuestieCompat.GetContainerItemInfo(bag, slot)) + if itemId == sourceItemId then + return true + end + end + + sourceItem = false + end + + -- If we are missing the sourceItem for zero objective quests then make an objective for it so the + -- player has a visual indication as to what item is missing and so the quest has a "tag" of some kind. + -- Also double check the quests leaderboard and make sure an objective doesn't already exist. + if (not sourceItem) and makeObjective and (not QuestieQuest:GetAllLeaderBoardDetails(quest.Id)[1]) then + local itemName = QuestieDB.QueryItemSingle(sourceItemId, "name") or ("Item " .. tostring(sourceItemId)) + quest.Objectives = { + [1] = { + Description = itemName, + Type = "item", + Needed = 1, + Collected = 0, + Completed = false, + Id = sourceItemId, + questId = quest.Id + } + } + end + else + return true + end + + return false +end + +local function _GetIconScaleForAvailable() + return Questie.db.profile.availableScale or 1.3 +end + +---@param quest Quest +function QuestieQuest:AddFinisher(quest) + --We should never ever add the quest if IsQuestFlaggedComplete true. + local questId = quest.Id + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Adding finisher for quest", questId) + + local complete = QuestieDB.IsComplete(questId) + + if (QuestiePlayer.currentQuestlog[questId] and (IsQuestFlaggedCompleted(questId) == false) and (complete == 1 or complete == 0) and (not Questie.db.char.complete[questId])) then + local finisher, key + + if quest.Finisher ~= nil then + if quest.Finisher.Type == "monster" then + finisher = QuestieDB:GetNPC(quest.Finisher.Id) + key = "m_" .. quest.Finisher.Id + elseif quest.Finisher.Type == "object" then + finisher = QuestieDB:GetObject(quest.Finisher.Id) + key = "o_" .. quest.Finisher.Id + else + Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieQuest] Unhandled finisher type:", quest.Finisher.Type, + questId, quest.name) + end + else + Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieQuest] Quest has no finisher:", questId, quest.name) + end + + if finisher ~= nil then + -- Certain race conditions can occur when the NPC/Objects are both the Quest Starter and Quest Finisher + -- which can result in duplicate Quest Title tooltips appearing. DrawAvailableQuest() would have already + -- registered this NPC/Object so, the appropriate tooltip lines are already present. This checks and clears + -- any duplicate keys before registering the Quest Finisher. + + -- Clear duplicate keys if they exist + if QuestieTooltips.lookupByKey[key] then + if QuestieTooltips:GetTooltip(key) ~= nil and #QuestieTooltips:GetTooltip(key) > 1 then + for ttline = 1, #QuestieTooltips:GetTooltip(key) do + for index, line in pairs(QuestieTooltips:GetTooltip(key)) do + if (ttline == index) then + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest] AddFinisher - Removing duplicate Quest Title!") + + -- Remove duplicate Quest Title + QuestieTooltips.lookupByKey[key][tostring(questId) .. " " .. finisher.name] = nil + + -- Now check to see if the dup has a Special Objective + local objText = string.match(line, ".*|cFFcbcbcb.*") + + if objText then + local objIndex + + -- Grab the Special Objective index + if quest.SpecialObjectives[1] then + objIndex = quest.SpecialObjectives[1].Index + end + + if objIndex then + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest] AddFinisher - Removing Special Objective!") + + -- Remove Special Objective Text + QuestieTooltips.lookupByKey[key][tostring(questId) .. " " .. objIndex] = nil + end + end + end + end + end + end + end + + QuestieTooltips:RegisterQuestStartTooltip(questId, finisher.name, finisher.id, key) + + local finisherIcons = {} + local finisherLocs = {} + + for finisherZone, spawns in pairs(finisher.spawns or {}) do + if (finisherZone ~= nil and spawns ~= nil) then + for _, coords in ipairs(spawns) do + local data = { + Id = questId, + Icon = Questie.ICON_TYPE_COMPLETE, + GetIconScale = _GetIconScaleForAvailable, + IconScale = _GetIconScaleForAvailable(), + Type = "complete", + QuestData = quest, + Name = finisher.name, + IsObjectiveNote = false, + } + + if QuestieDB.IsActiveEventQuest(quest.Id) then + data.Icon = Questie.ICON_TYPE_EVENTQUEST_COMPLETE + elseif QuestieDB.IsPvPQuest(quest.Id) then + data.Icon = Questie.ICON_TYPE_PVPQUEST_COMPLETE + elseif quest.IsRepeatable then + data.Icon = Questie.ICON_TYPE_REPEATABLE_COMPLETE + end + + if (coords[1] == -1 or coords[2] == -1) then + local dungeonLocation = ZoneDB:GetDungeonLocation(finisherZone) + if dungeonLocation ~= nil then + for _, value in ipairs(dungeonLocation) do + local zone = value[1]; + local x = value[2]; + local y = value[3]; + + QuestieMap:DrawWorldIcon(data, zone, x, y) + end + end + else + local x = coords[1]; + local y = coords[2]; + + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] Adding world icon as finisher:", + finisherZone, x, y) + finisherIcons[finisherZone] = QuestieMap:DrawWorldIcon(data, finisherZone, x, y) + + if not finisherLocs[finisherZone] then + finisherLocs[finisherZone] = { x, y } + end + end + end + end + end + + if finisher.waypoints then + for zone, waypoints in pairs(finisher.waypoints) do + if (not ZoneDB.IsDungeonZone(zone)) then + if not finisherIcons[zone] and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then + local data = { + Id = questId, + Icon = Questie.ICON_TYPE_COMPLETE, + GetIconScale = _GetIconScaleForAvailable, + IconScale = _GetIconScaleForAvailable(), + Type = "complete", + QuestData = quest, + Name = finisher.name, + IsObjectiveNote = false, + } + + if QuestieDB.IsActiveEventQuest(quest.Id) then + data.Icon = Questie.ICON_TYPE_EVENTQUEST_COMPLETE + elseif QuestieDB.IsPvPQuest(quest.Id) then + data.Icon = Questie.ICON_TYPE_PVPQUEST_COMPLETE + elseif quest.IsRepeatable then + data.Icon = Questie.ICON_TYPE_REPEATABLE_COMPLETE + end + + finisherIcons[zone] = QuestieMap:DrawWorldIcon(data, zone, waypoints[1][1][1], + waypoints[1][1][2]) + finisherLocs[zone] = { waypoints[1][1][1], waypoints[1][1][2] } + end + + QuestieMap:DrawWaypoints(finisherIcons[zone], waypoints, zone) + end + end + end + else + Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieQuest] finisher or finisher.spawns == nil for questId", + questId) + end + end +end + +---@param quest Quest +---@param objectiveIndex ObjectiveIndex +---@param objective QuestObjective +---@param blockItemTooltips any +function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockItemTooltips) -- must be p-called + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjective]", objective.Description) + + if (not objective.Update) then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjective] - Quest is already updated. --> Exiting!") + return + end + + objective:Update() + local completed = objective.Completed + local objectiveData = quest.ObjectiveData[objective.Index] or + objective -- the reason for "or objective" is to handle "SpecialObjectives" aka non-listed objectives (demonic runestones for closing the portal) + + if (not objective.spawnList or (not next(objective.spawnList))) and _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type] then + objective.spawnList = _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type](objective.Id, objective, + objectiveData); + end + + -- Tooltips should always show. + -- For completed and uncompleted objectives + _RegisterObjectiveTooltips(objective, quest.Id, blockItemTooltips) + + if completed or quest.isComplete then + _UnloadAlreadySpawnedIcons(objective) + return + end + + if (not objective.Color) then + objective.Color = QuestieLib:ColorWheel() + end + + if objective.spawnList and next(objective.spawnList) then + local maxPerType = 300 + + if Questie.db.profile.enableIconLimit and Questie.db.profile.iconLimit < maxPerType then + maxPerType = Questie.db.profile.iconLimit + end + + local closestStarter = QuestieMap:FindClosestStarter() + local objectiveCenter = closestStarter[quest.Id] + + local zoneCount = 0 + local zones = {} + local objectiveZone + + for _, spawnData in pairs(objective.spawnList) do + for zone in pairs(spawnData.Spawns) do + zones[zone] = true + end + end + + for zone in pairs(zones) do + objectiveZone = zone + zoneCount = zoneCount + 1 + end + + if zoneCount == 1 then -- this objective happens in 1 zone, clustering should be relative to that zone + local x, y = HBD:GetWorldCoordinatesFromZone(0.5, 0.5, ZoneDB:GetUiMapIdByAreaId(objectiveZone)) + objectiveCenter = { x = x, y = y } + end + + local iconsToDraw, _ = _DetermineIconsToDraw(quest, objective, objectiveIndex, objectiveCenter) + local icon, iconPerZone = _DrawObjectiveIcons(quest.Id, iconsToDraw, objective, maxPerType) + _DrawObjectiveWaypoints(objective, icon, iconPerZone) + end +end + +_RegisterObjectiveTooltips = function(objective, questId, blockItemTooltips) + Questie:Debug(Questie.DEBUG_INFO, "Registering objective tooltips for", objective.Description) + + if objective.spawnList then + if (not objective.hasRegisteredTooltips) then + for id, spawnData in pairs(objective.spawnList) do + if spawnData.TooltipKey and (not objective.AlreadySpawned[id]) then + QuestieTooltips:RegisterObjectiveTooltip(questId, spawnData.TooltipKey, objective) + end + end + + objective.hasRegisteredTooltips = true + end + else + -- No spawnList and no Id means there is nothing Questie can draw for this objective. + -- This covers server-tracked trigger objectives (e.g. "complete N quests in zone" for + -- quest 50150) which may have any objectiveType from the server, not just "event". + if not objective.Id then + objective.hasRegisteredTooltips = true + return + end + Questie:Error("[QuestieQuest]: [Tooltips] " .. + l10n("There was an error populating objectives for %s %s %s %s", objective.Description or "No objective text", + questId or "No quest id", 0 or "No objective", "No error")) + end + + if (not objective.registeredItemTooltips) and objective.Type == "item" and (not blockItemTooltips) and objective.Id then + local itemName = QuestieDB.QueryItemSingle(objective.Id, "name") + + if itemName then + QuestieTooltips:RegisterObjectiveTooltip(questId, "i_" .. objective.Id, objective) + end + + objective.registeredItemTooltips = true + end +end + +_UnloadAlreadySpawnedIcons = function(objective) + if objective.AlreadySpawned and next(objective.AlreadySpawned) then + for id, spawn in pairs(objective.AlreadySpawned) do + if spawn then + for _, mapIcon in pairs(spawn.mapRefs) do + mapIcon:Unload() + end + for _, minimapIcon in pairs(spawn.minimapRefs) do + minimapIcon:Unload() + end + spawn.mapRefs = {} + spawn.minimapRefs = {} + end + end + objective.AlreadySpawned = {} + end +end + +---@param quest Quest +---@param objective QuestObjective +---@param objectiveIndex ObjectiveIndex +---@param objectiveCenter {x:X, y:Y} +_DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCenter) + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DetermineIconsToDraw]") + + local iconsToDraw = {} + local spawnItemId + + for id, spawnData in pairs(objective.spawnList) do + if spawnData.ItemId then + spawnItemId = spawnData.ItemId + end + + if (not objective.Icon) and spawnData.Icon then + objective.Icon = spawnData.Icon + end + + if (not objective.AlreadySpawned[id]) and (not objective.Completed) and Questie.db.profile.enableObjectives then + local data = { + Id = quest.Id, + ObjectiveIndex = objectiveIndex, + QuestData = quest, + ObjectiveData = objective, + Icon = spawnData.Icon, + IconColor = quest.Color, + GetIconScale = spawnData.GetIconScale, + IconScale = spawnData.GetIconScale(), + Name = spawnData.Name, + Type = objective.Type, + ObjectiveTargetId = spawnData.Id + } + + objective.AlreadySpawned[id] = { + data = data, + minimapRefs = {}, + mapRefs = {}, + } + + for zone, spawns in pairs(spawnData.Spawns) do + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + for _, spawn in pairs(spawns) do + if (spawn[1] and spawn[2]) then + local drawIcon = { + AlreadySpawnedId = id, + data = data, + zone = zone, + AreaID = zone, + UiMapID = uiMapId, + x = spawn[1], + y = spawn[2], + worldX = 0, + worldY = 0, + distance = 0, + touched = nil, -- TODO change. This is meant to let lua reserve memory for all keys needed for sure. + } + local x, y, _ = HBD:GetWorldCoordinatesFromZone(drawIcon.x / 100, drawIcon.y / 100, uiMapId) + x = x or 0 + y = y or 0 + -- Cache world coordinates for clustering calculations + drawIcon.worldX = x + drawIcon.worldY = y + -- There are instances when X and Y are not in the same map such as in dungeons etc, we default to 0 if it is not set + -- This will create a distance of 0 but it doesn't matter. + local distance = QuestieLib:Euclid(objectiveCenter.x or 0, objectiveCenter.y or 0, x, y); + drawIcon.distance = distance or 0 -- cache for clustering + -- there can be multiple icons at same distance at different directions + --local distance = floor(distance) + local iconList = iconsToDraw[distance] + if iconList then + iconList[#iconList + 1] = drawIcon + else + iconsToDraw[distance] = { drawIcon } + end + end + end + end + end + end + + return iconsToDraw, spawnItemId +end + +_DrawObjectiveIcons = function(questId, iconsToDraw, objective, maxPerType) + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveIcons] Adding Icons for quest:", questId) + + local spawnedIconCount = 0 + local icon + local iconPerZone = {} + + local range = Questie.db.profile.clusterLevelHotzone + + local iconCount, orderedList = _GetIconsSortedByDistance(iconsToDraw) + + if orderedList[1] and orderedList[1].Icon == Questie.ICON_TYPE_OBJECT then -- new clustering / limit code should prevent problems, always show all object notes + range = range * 0.2; -- Only use 20% of the default range. + end + + local hotzones = QuestieMap.utils:CalcHotzones(orderedList, range, iconCount); + + for i = 1, #hotzones do + local hotzone = hotzones[i] + if (spawnedIconCount > maxPerType) then + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] Too many icons for quest:", questId) + break; + end + + --Any icondata will do because they are all the same + icon = hotzone[1]; + + local spawnsMapRefs = objective.AlreadySpawned[icon.AlreadySpawnedId].mapRefs + local spawnsMinimapRefs = objective.AlreadySpawned[icon.AlreadySpawnedId].minimapRefs + + local centerX, centerY = QuestieMap.utils.CenterPoint(hotzone) + + local dungeonLocation = ZoneDB:GetDungeonLocation(icon.zone) + + if dungeonLocation and centerX == -1 and centerY == -1 then + if dungeonLocation[2] then -- We have more than 1 instance entrance (e.g. Blackrock dungeons) + local secondDungeonLocation = dungeonLocation[2] + + icon.zone = secondDungeonLocation[1] + centerX = secondDungeonLocation[2] + centerY = secondDungeonLocation[3] + + local iconMap, iconMini = QuestieMap:DrawWorldIcon(icon.data, icon.zone, centerX, centerY) -- clustering code takes care of duplicates as long as min-dist is more than 0 + + if iconMap and iconMini then + iconPerZone[icon.zone] = { iconMap, centerX, centerY } + spawnsMapRefs[#spawnsMapRefs + 1] = iconMap + spawnsMinimapRefs[#spawnsMinimapRefs + 1] = iconMini + end + + spawnedIconCount = spawnedIconCount + 1; + end + + local firstDungeonLocation = dungeonLocation[1] + icon.zone = firstDungeonLocation[1] + centerX = firstDungeonLocation[2] + centerY = firstDungeonLocation[3] + end + + local iconMap, iconMini = QuestieMap:DrawWorldIcon(icon.data, icon.zone, centerX, centerY) -- clustering code takes care of duplicates as long as min-dist is more than 0 + + if iconMap and iconMini then + iconPerZone[icon.zone] = { iconMap, centerX, centerY } + spawnsMapRefs[#spawnsMapRefs + 1] = iconMap + spawnsMinimapRefs[#spawnsMinimapRefs + 1] = iconMini + end + + spawnedIconCount = spawnedIconCount + 1; + end + + return icon, iconPerZone +end + +_GetIconsSortedByDistance = function(icons) + local iconCount = 0; + local orderedList = {} + local distances = {} + + local i = 0 + + for distance in pairs(icons) do + i = i + 1 + distances[i] = distance + end + + table.sort(distances) + + -- use the keys to retrieve the values in the sorted order + for distIndex = 1, #distances do + local iconsAtDisntace = icons[distances[distIndex]] + + for iconIndex = 1, #iconsAtDisntace do + local icon = iconsAtDisntace[iconIndex] + + iconCount = iconCount + 1 + orderedList[iconCount] = icon + end + end + + return iconCount, orderedList +end + +_DrawObjectiveWaypoints = function(objective, icon, iconPerZone) + for _, spawnData in pairs(objective.spawnList) do -- spawnData.Name, spawnData.Spawns + if spawnData.Waypoints then + for zone, waypoints in pairs(spawnData.Waypoints) do + local firstWaypoint = waypoints[1][1] + + if (not iconPerZone[zone]) and icon and firstWaypoint[1] ~= -1 and firstWaypoint[2] ~= -1 then -- spawn an icon in this zone for the mob + local iconMap, iconMini = QuestieMap:DrawWorldIcon(icon.data, zone, firstWaypoint[1], + firstWaypoint[2]) -- clustering code takes care of duplicates as long as min-dist is more than 0 + + if iconMap and iconMini then + iconPerZone[zone] = { iconMap, firstWaypoint[1], firstWaypoint[2] } + tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].mapRefs, iconMap); + tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].minimapRefs, iconMini); + end + end + + local ipz = iconPerZone[zone] + + if ipz then + QuestieMap:DrawWaypoints(ipz[1], waypoints, zone, spawnData.Hostile and { 1, 0.2, 0, 0.7 } or nil) + end + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveWaypoints]") + end + end +end + +---@param quest Quest +function QuestieQuest:PopulateObjectiveNotes(quest) -- this should be renamed to PopulateNotes as it also handles finishers now + if (not quest) then + return + end + + if QuestieDB.IsComplete(quest.Id) == 1 then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjectiveNotes] Quest Complete! Adding Finisher for:", + quest.Id) + + QuestieQuest:UpdateQuest(quest.Id) + _AddSourceItemObjective(quest) + _AddRequiredSourceItemObjective(quest) + _AddSpellItemObjective(quest) + + return + end + + if (not quest.Color) then + quest.Color = QuestieLib:ColorWheel() + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjectiveNotes] Populating objectives for:", quest.Id) + + QuestieQuest:UpdateObjectiveNotes(quest) + _AddSourceItemObjective(quest) + _AddRequiredSourceItemObjective(quest) + _AddSpellItemObjective(quest) +end + +---@param quest Quest +---@return true? +function QuestieQuest:PopulateQuestLogInfo(quest) + if (not quest) then + return nil + end + + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateQuestLogInfo] ", quest.Id) + + local questLogEngtry = QuestLogCache.GetQuest(quest.Id) -- DO NOT MODIFY THE RETURNED TABLE + + if (not questLogEngtry) then return end + + if questLogEngtry.isComplete ~= nil and questLogEngtry.isComplete == 1 then + quest.isComplete = true + end + + --Uses the category order to draw the quests and trusts the database order. + + local questObjectives = QuestieQuest:GetAllLeaderBoardDetails(quest.Id) or {} -- DO NOT MODIFY THE RETURNED TABLE + + for objectiveIndex, objective in pairs(questObjectives) do + if objective.type and string.len(objective.type) > 1 then + if (not quest.ObjectiveData) or (not quest.ObjectiveData[objectiveIndex]) then + Questie:Error(l10n("Missing objective data for quest "), quest.Id, " ", objective.text) + else + if not quest.Objectives[objectiveIndex] then + quest.Objectives[objectiveIndex] = { + Id = quest.ObjectiveData[objectiveIndex].Id, + Index = objectiveIndex, + questId = quest.Id, + _lastUpdate = 0, + Description = objective.text, + spawnList = {}, + AlreadySpawned = {}, + Update = _QuestieQuest.ObjectiveUpdate, + Coordinates = quest.ObjectiveData[objectiveIndex].Coordinates, -- Only for type "event" + RequiredRepValue = quest.ObjectiveData[objectiveIndex].RequiredRepValue + } + end + + quest.Objectives[objectiveIndex]:Update() + end + end + + if (not quest.Objectives[objectiveIndex]) or (not quest.Objectives[objectiveIndex].Id) then + Questie:Debug(Questie.DEBUG_DEVELOP, + "[QuestieQuest:PopulateQuestLogInfo] Error finding entry ID for objective", objectiveIndex, + objective.type, objective.text, "of questId:", quest.Id) + end + end + + -- find special unlisted objectives + if next(quest.SpecialObjectives) then + for index, specialObjective in pairs(quest.SpecialObjectives) do + if (not specialObjective.Description) then + specialObjective.Description = "Special objective" + end + + specialObjective.questId = quest.Id + + if specialObjective.RealObjectiveIndex and quest.Objectives[specialObjective.RealObjectiveIndex] then + -- This specialObjective is an extraObjective and has a RealObjectiveIndex set + specialObjective.Completed = quest.Objectives[specialObjective.RealObjectiveIndex].Completed + specialObjective.Update = function(self) + self.Completed = quest.Objectives[self.RealObjectiveIndex].Completed + end + else + specialObjective.Update = NOP_FUNCTION + end + + specialObjective.Index = 64 + index -- offset to not conflict with real objectives + specialObjective.AlreadySpawned = specialObjective.AlreadySpawned or {} + end + end + + if #quest.Objectives == 0 and #quest.SpecialObjectives == 0 and (not quest.ObjectiveData or #quest.ObjectiveData == 0) and ((quest.triggerEnd and #quest.triggerEnd > 0) or (quest.Finisher and quest.Finisher.Id ~= nil)) then + -- Some quests when picked up will be flagged isComplete == 0 but the quest.Objective table or quest.SpecialObjectives table is nil. This + -- check assumes the Quest should have been flagged questLogEngtry.isComplete == 1. We're specifically looking for a quest.triggerEnd or + -- a quest.Finisher.Id because this might throw an error if there is nothing to populate when we call QuestieQuest:AddFinisher(). + -- We added a check for quest.ObjectiveData to ensure we don't prematurely complete quests that are just waiting for the server to sync their objectives. + QuestieMap:UnloadQuestFrames(quest.Id) + QuestieQuest:AddFinisher(quest) + quest.isComplete = true + end + + return true +end + +---@param self QuestObjective @quest.Objectives[] entry +function _QuestieQuest.ObjectiveUpdate(self) + if self.isUpdated then + return + end + + local questObjectives = QuestieQuest:GetAllLeaderBoardDetails(self.questId) -- DO NOT MODIFY THE RETURNED TABLE + + if questObjectives and questObjectives[self.Index] then + local obj = questObjectives[self.Index] -- DO NOT EDIT THE TABLE + if (obj.type) then + -- fixes for api bug + local numFulfilled = obj.numFulfilled or 0 + local numRequired = obj.numRequired or 0 + local finished = obj.finished or false -- ensure its boolean false and not nil (hack) + + self.Type = obj.type; + self.Description = obj.text + self.Collected = tonumber(numFulfilled); + self.Needed = tonumber(numRequired); + self.Completed = (self.Needed == self.Collected and self.Needed > 0) or + (finished and (self.Needed == 0 or (not self.Needed))) -- some objectives get removed on PLAYER_LOGIN because isComplete is set to true at random???? + -- Mark objective updated + self.isUpdated = true + end + end +end + +---@param questId number +---@return table|nil @DO NOT EDIT RETURNED TABLE +function QuestieQuest:GetAllLeaderBoardDetails(questId) + Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:GetAllLeaderBoardDetails] for questId", questId) + + local questObjectives = QuestLogCache.GetQuestObjectives(questId) -- DO NOT MODIFY THE RETURNED TABLE + if (not questObjectives) then return end + + for _, objective in pairs(questObjectives) do -- DO NOT MODIFY THE RETURNED TABLE + -- TODO Move this to QuestEventHandler module or QuestieQuest:AcceptQuest( ) + QuestieQuest:UpdateQuest( ) (accept quest one required to register objectives without progress) + -- TODO After ^^^ moving remove this function and use "QuestLogCache.GetQuest(questId).objectives -- DO NOT MODIFY THE RETURNED TABLE" in place of it. + QuestieAnnounce:ObjectiveChanged(questId, objective.text, objective.numFulfilled, objective.numRequired) + end + + return questObjectives +end + +function QuestieQuest.DrawDailyQuest(questId) + if QuestieDB.IsDoable(questId) then + local quest = QuestieDB.GetQuest(questId) + AvailableQuests.DrawAvailableQuest(quest) + end +end + +return QuestieQuest diff --git a/.history/Questie-335_20260222000735.toc b/.history/Questie-335_20260222000735.toc new file mode 100644 index 0000000..45aae64 --- /dev/null +++ b/.history/Questie-335_20260222000735.toc @@ -0,0 +1,250 @@ +## Interface: 30300 +## Title: Questie - Ebonhold +## Author: Questie Team +## Notes: A standalone Classic QuestHelper +## Notes-esMX: Ayundante de misión +## Notes-esES: Ayundante de misión +## Notes-ptBR: Ajudante de missão +## Notes-frFR: Assistant de quête +## Version: 9.7.12 +## RequiredDeps: +## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu +## SavedVariables: QuestieConfig +## SavedVariablesPerCharacter: QuestieConfigCharacter +## X-Curse-Project-ID: 334372 +## X-Wago-ID: qv634BKb +## X-WOW_PROJECT_ID: 11 + +# Loader module +Modules\Libs\QuestieLoader.lua + +# COMPATIBILITY +Modules\QuestieCompat.lua +Compat\embeds.xml + +Modules\VersionCheck.lua + +# Thread Manager +Modules\Libs\ThreadLib.lua +#Message Handler +Modules\Libs\MessageHandler.lua + +#Quest XP +Database\QuestXP\QuestieXP.lua +Database\QuestXP\DB\xpDB-wotlk.lua + +# stream module (used by DB) +Modules\QuestieStream.lua + +# Zones +Database\Zones\zoneTables.lua +# Ascension Custom Zones +Database\Ascension\Zones\AscensionZoneTables.lua +Database\Ascension\Zones\AscensionUiMapData.lua +# Ebonhold Custom Zones +Database\Ebonhold\Zones\EbonholdZoneTables.lua +Database\Ebonhold\Zones\EbonholdUiMapData.lua +Database\Zones\zoneDB.lua + + +# Databases +Database\Wotlk\wotlkItemDB.lua +Database\Wotlk\wotlkNpcDB.lua +Database\Wotlk\wotlkObjectDB.lua +Database\Wotlk\wotlkQuestDB.lua +Database\QuestieDB.lua +Database\questDB.lua +Database\objectDB.lua +Database\npcDB.lua +Database\itemDB.lua +Database\Constants.lua +Database\MeetingStones.lua + +# Ascension Custom Database +Database\Ascension\Bronzebeard\AscensionNpcDB.lua +Database\Ascension\Bronzebeard\AscensionObjectDB.lua +Database\Ascension\Bronzebeard\AscensionItemDB.lua +Database\Ascension\Bronzebeard\AscensionQuestDB.lua +Database\Ascension\AscensionLoader.lua + +# Ebonhold Custom Database +Database\Ebonhold\Ebonhold\EbonholdNpcDB.lua +Database\Ebonhold\Ebonhold\EbonholdObjectDB.lua +Database\Ebonhold\Ebonhold\EbonholdItemDB.lua +Database\Ebonhold\Ebonhold\EbonholdQuestDB.lua +Database\Ebonhold\EbonholdLoader.lua + + +# Corrections +Database\Corrections\AutoTableUpdates.lua +Database\Corrections\QuestieCorrections.lua +Database\Corrections\QuestieItemBlacklist.lua +Database\Corrections\QuestieNPCBlacklist.lua +Database\Corrections\QuestieQuestBlacklist.lua +#Database\Corrections\SoMPhases.lua +Database\Corrections\QuestieEvent.lua + +# Automatic General Corrections +Database\Corrections\Automatic\itemStartFixes.lua +Database\Corrections\Automatic\classicQuestReputationFixes.lua + +# Classic Corrections +Database\Corrections\classicQuestFixes.lua +Database\Corrections\classicNPCFixes.lua +Database\Corrections\classicItemFixes.lua +Database\Corrections\classicObjectFixes.lua + +# TBC Corrections +Database\Corrections\tbcQuestFixes.lua +Database\Corrections\tbcNPCFixes.lua +Database\Corrections\tbcItemFixes.lua +Database\Corrections\tbcObjectFixes.lua + +# Wotlk Corrections +Database\Corrections\wotlkQuestFixes.lua +Database\Corrections\wotlkNPCFixes.lua +Database\Corrections\wotlkItemFixes.lua +Database\Corrections\wotlkObjectFixes.lua + +# Compiler +Database\compiler.lua + +# Localization +Localization\l10n.lua +Localization\Translations\Translations.xml + +Localization\lookups\lookupQuestCategories.lua +Localization\lookups\lookupZones.lua +Localization\lookups\Wotlk\lookupItems\lookupItems.xml +Localization\lookups\Wotlk\lookupNpcs\lookupNpcs.xml +Localization\lookups\Wotlk\lookupObjects\lookupObjects.xml +Localization\lookups\Wotlk\lookupQuests\lookupQuests.xml + +# Libs +Modules\Libs\QuestieLib.lua +Modules\Libs\QuestieSerializer.lua +Modules\Libs\QuestieCombatQueue.lua +Modules\Libs\RamerDouglasPeucker.lua + +# Modules +Modules\QuestieValidateGameCache.lua +Modules\Arrow\QuestieArrow.lua +Modules\QuestieInit.lua +Modules\MinimapIcon.lua +Modules\QuestieProfessions.lua +Modules\QuestieReputation.lua +Modules\QuestieEventHandler.lua +Modules\QuestieLearner.lua +Modules\Migration.lua +Modules\QuestieMenu\Townsfolk.lua +Modules\QuestieMenu\ClassTrainers.lua +Modules\QuestieMenu\Mailboxes.lua +Modules\QuestieMenu\MeetingStones.lua +Modules\QuestieMenu\ProfessionTrainers.lua +Modules\QuestieMenu\QuestieMenu.lua +Modules\QuestieAnnounce.lua +Modules\QuestieShutUp.lua +Modules\Sounds.lua +Modules\TaskQueue.lua +Modules\QuestiePlayer.lua +#Modules\QuestieDebugOffer.lua +Modules\WorldMapButton\WorldMapButton.lua +#Modules\WorldMapButton\QuestieWorldMapButtonTemplate.xml + +# QuestLinks +Modules\QuestLinks\ChatFilter.lua +Modules\QuestLinks\Hooks.lua +Modules\QuestLinks\Link.lua + +# Tooltips +Modules\Tooltips\Tooltip.lua +Modules\Tooltips\MapIconTooltip.lua +Modules\Tooltips\TooltipHandler.lua + +# Auto +Modules\Auto\QuestieAuto.lua +Modules\Auto\Privates.lua +Modules\Auto\DisallowedIDs.lua + +# FramePool +Modules\FramePool\QuestieFramePool.lua +Modules\FramePool\QuestieFrame.lua + +# Map +Modules\Map\QuestieMap.lua +Modules\Map\QuestieMapUtils.lua +#Modules\Map\HBDHooks.lua +Modules\Map\WeaponMasterSkills.lua + + +# Quest +Modules\Quest\AvailableQuests.lua +Modules\Quest\QuestLogCache.lua +Modules\Quest\DailyQuests.lua +Modules\Quest\IsleOfQuelDanas.lua +Modules\Quest\QuestEventHandler.lua +Modules\Quest\QuestgiverFrame.lua +Modules\Quest\QuestieQuest.lua +Modules\Quest\QuestieQuestPrivates.lua + +Modules\QuestieNameplate.lua +Modules\QuestieCoordinates.lua + +# Network +Modules\Network\QuestieComms.lua +Modules\Network\QuestieCommsData.lua + +# Journey +Modules\Journey\QuestieJourney.lua +Modules\Journey\QuestieJourneyPrivates.lua +Modules\Journey\QuestieJourneyUtils.lua +Modules\Journey\History.lua +Modules\Journey\QuestDetailsFrame.lua +Modules\Journey\tabs\MyJourney\Note.lua +Modules\Journey\tabs\MyJourney\MyJourney.lua +Modules\Journey\tabs\MyJourney\MyJourneyTab.lua +Modules\Journey\tabs\QuestsByZone\QuestsByZone.lua +Modules\Journey\tabs\QuestsByZone\QuestsByZoneTab.lua +#Modules\Journey\tabs\Search\Search.lua +#Modules\Journey\tabs\Search\SearchTab.lua +Modules\Journey\QuestieSearch.lua +Modules\Journey\QuestieSearchResults.lua + +# Tracker +Modules\Tracker\QuestieTracker.lua +Modules\Tracker\TrackerUtils.lua +Modules\Tracker\TrackerMenu.lua +Modules\Tracker\TrackerFadeTicker.lua +Modules\Tracker\TrackerBaseFrame.lua +Modules\Tracker\TrackerHeaderFrame.lua +Modules\Tracker\TrackerQuestFrame.lua +Modules\Tracker\TrackerQuestTimers.lua +Modules\Tracker\TrackerLinePool.lua + +# Tutorial +Modules\Tutorial\ChooseObjectiveType.lua +Modules\Tutorial\Tutorial.lua + +#Modules\QuestieDBMIntegration.lua +Modules\QuestieSlash.lua + +# Options +Modules\Options\QuestieOptions.lua +Modules\Options\QuestieOptionsDefaults.lua +Modules\Options\QuestieOptionsUtils.lua +Modules\Options\AdvancedTab\QuestieOptionsAdvanced.lua +Modules\Options\AutoTab\QuestieOptionsAuto.lua +Modules\Options\DBMTab\QuestieOptionsDBM.lua +Modules\Options\GeneralTab\QuestieOptionsGeneral.lua +Modules\Options\IconsTab\QuestieOptionsIcons.lua +Modules\Options\NameplateTab\QuestieOptionsNameplate.lua +Modules\Options\TrackerTab\QuestieOptionsTracker.lua + +# Cleanup +Modules\QuestieCleanup.lua + +# Profiler +Modules\QuestieProfiler.lua + +# Main +Questie.lua diff --git a/.history/README_20260222000744.md b/.history/README_20260222000744.md new file mode 100644 index 0000000..df1cfc2 --- /dev/null +++ b/.history/README_20260222000744.md @@ -0,0 +1,105 @@ +
+ +# Questie (3.3.5a) + +![Version](https://img.shields.io/badge/version-v9.7.12-blue.svg?style=for-the-badge) +![Downloads](https://img.shields.io/github/downloads/Xurkon/PE-Questie/total?style=for-the-badge&color=e67e22) +[![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/PE-Questie/) +[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon) +[![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/Xurkon) +![License](https://img.shields.io/github/license/Xurkon/PE-Questie?style=for-the-badge&color=2980b9) +![WoW](https://img.shields.io/badge/WoW-3.3.5a-blue?style=for-the-badge&logo=world-of-warcraft&logoColor=white) + +
+**A fork of the WoW Classic Questie addon aiming to provide compatibility with ANY private server.** + +[⬇ **Download Latest**](https://github.com/Xurkon/PE-Questie/releases/latest)   •   [📂 **View Source**](https://github.com/Xurkon/PE-Questie)   •   [📖 **Read Documentation**](https://xurkon.github.io/PE-Questie/) + +
+ +--- + +## 📥 Installation + +1. [Download](https://github.com/Xurkon/PE-Questie/releases) the archive. +2. Extract it into the `Interface/AddOns/` directory. The folder name should be `Questie-335`. +3. **Custom Server Support**: If you are playing on a server emulating a previous expansion (Classic or TBC) using the 3.3.5 client, you can add `-Classic` or `-TBC` to the folder name to load specific datasets. +4. **Map Compatibility**: If your server lacks a world map patch, enable `Options → Advanced → Use WotLK map data`. + +--- + +## 🔧 Fixes & Compatibility + +### 🛡️ Nameplates +- Explicitly skips **Ascension Nameplates** to avoid conflicts, while maintaining compatibility with generic nameplate addons. + +### 📊 Quest Tracker +- **Ascension API**: Fully compatible with custom quest APIs; no crashes on auto-turn-in quests. +- **Header Persistence**: Resolved issues where quest headers would disappear from the tracker. +- **Dynamic Updates**: Instant refresh when accepting, completing, or abandoning quests. +- **Combat Safety**: Protected with `pcall` to prevent UI lockups during intense combat updates. + +### 💬 Tooltips +- Fixed all legacy Lua errors. +- **New Feature**: Displays if an NPC drops an item that starts a quest directly in the tooltip. + +### 🗺️ Maps (Minimap & World Map) +- **Minimap**: Fixed zoom-related Lua errors. +- **World Map**: Full support for Ascension's `WorldMapFrame` (minimized mode), **Mapster**, and **Magnify-WotLK**. +- **Icon Cleanup**: Resolved "ghost icon" bug where completed quests remained visible on the map. + +### 📦 Custom IDs +- Native support for large integer IDs common on custom private servers. + +--- + +## ✨ Features + +### ⚔️ Ascension Scaling +- Quests automatically scale to character level, perfectly matching the Ascension Scaling system. + +### 📍 Visual Map Objectives +Notes for quest starters, turn-ins, and complex objectives are drawn directly on your maps. + +
+ Quest Givers + Quest Complete + Quest Tooltip +
+ +### 📜 Advanced Quest Tracker +- **Smart Tracking**: Automatically tracks quests upon acceptance. +- **Expanded Capacity**: Displays up to 20 quests (original limit: 5). +- **Interactive**: Left-click to open the log; Right-click for focus modes or TomTom arrow integration. + +
+ Tracker +
+ +### 🗺️ My Journey & Quests by Zone +- **Journey Log**: Record every major step of your adventure. +- **Completionist View**: Lists all available and completed quests per zone to ensure nothing is missed. + +
+ Journey + Zone Quests +
+ +### 🔍 Database Search & Config +- **Global Search**: Find any NPC, Object, or Quest in the massive Questie database. +- **Deep Customization**: Adjust everything from icon scale to tracking logic. + +
+ Search + Config +
+ +--- + +## 👥 Credits +- **Questie Team** - Original addon developers. +- **Xurkon** - Private Expansion fork and maintenance. +- **Project Ascension & Ebonhold Communities** - Testing and data feedback. + +## 📄 License +MIT License - See [LICENSE](LICENSE) for details. diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fb7ac..a25c407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v9.7.12 + +### Fixes +- **[Quest]** Fixed an issue where tracking icons for dynamically updated custom quests like Peak Predators would prematurely disappear due to sync delays on WotLK servers. +- **[Map]** Resolved a frame pool leak that prevented the yellow Finisher icon from appearing immediately on the map after turning in or completing a quest. + + ## v9.7.11 ### Fixes diff --git a/Modules/FramePool/QuestieFramePool.lua b/Modules/FramePool/QuestieFramePool.lua index 0ed393d..076c9bd 100644 --- a/Modules/FramePool/QuestieFramePool.lua +++ b/Modules/FramePool/QuestieFramePool.lua @@ -100,6 +100,7 @@ function QuestieFramePool:GetFrame() returnFrame.loaded = true returnFrame.shouldBeShowing = nil returnFrame.hidden = nil + returnFrame._needsUnload = nil if returnFrame.BaseOnShow then returnFrame:SetScript("OnShow", returnFrame.BaseOnShow) diff --git a/Modules/Quest/QuestieQuest.lua b/Modules/Quest/QuestieQuest.lua index 1f21aa2..399a19a 100644 --- a/Modules/Quest/QuestieQuest.lua +++ b/Modules/Quest/QuestieQuest.lua @@ -1660,10 +1660,11 @@ function QuestieQuest:PopulateQuestLogInfo(quest) end end - if #quest.Objectives == 0 and #quest.SpecialObjectives == 0 and ((quest.triggerEnd and #quest.triggerEnd > 0) or (quest.Finisher and quest.Finisher.Id ~= nil)) then + if #quest.Objectives == 0 and #quest.SpecialObjectives == 0 and (not quest.ObjectiveData or #quest.ObjectiveData == 0) and ((quest.triggerEnd and #quest.triggerEnd > 0) or (quest.Finisher and quest.Finisher.Id ~= nil)) then -- Some quests when picked up will be flagged isComplete == 0 but the quest.Objective table or quest.SpecialObjectives table is nil. This -- check assumes the Quest should have been flagged questLogEngtry.isComplete == 1. We're specifically looking for a quest.triggerEnd or -- a quest.Finisher.Id because this might throw an error if there is nothing to populate when we call QuestieQuest:AddFinisher(). + -- We added a check for quest.ObjectiveData to ensure we don't prematurely complete quests that are just waiting for the server to sync their objectives. QuestieMap:UnloadQuestFrames(quest.Id) QuestieQuest:AddFinisher(quest) quest.isComplete = true diff --git a/Questie-335.toc b/Questie-335.toc index 125a148..45aae64 100644 --- a/Questie-335.toc +++ b/Questie-335.toc @@ -6,7 +6,7 @@ ## Notes-esES: Ayundante de misión ## Notes-ptBR: Ajudante de missão ## Notes-frFR: Assistant de quête -## Version: 9.7.11 +## Version: 9.7.12 ## RequiredDeps: ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## SavedVariables: QuestieConfig diff --git a/README.md b/README.md index 08d281a..df1cfc2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Questie (3.3.5a) -![Version](https://img.shields.io/badge/version-v9.7.11-blue.svg?style=for-the-badge) +![Version](https://img.shields.io/badge/version-v9.7.12-blue.svg?style=for-the-badge) ![Downloads](https://img.shields.io/github/downloads/Xurkon/PE-Questie/total?style=for-the-badge&color=e67e22) [![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/PE-Questie/) [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon)