--------------------------------------------------------------------------------------------------- --Name: QuestieQuest --Description: Handles all the quest related functions --------------------------------------------------------------------------------------------------- --///////////////////////////////////////////////////////////////////////////////////////////////-- --------------------------------------------------------------------------------------------------- --Local Vars --------------------------------------------------------------------------------------------------- local QuestieHashCache = {}; local LastNrOfEntries = 0; local CachedIds = {}; local QuestieQuestHashCache = {}; local QGet_TitleText = GetTitleText; local QGet_QuestLogTitle = GetQuestLogTitle; local QGet_NumQuestLeaderBoards = GetNumQuestLeaderBoards; local QGet_QuestLogLeaderBoard = GetQuestLogLeaderBoard; local QGet_QuestLogQuestText = GetQuestLogQuestText; local QGet_NumQuestLogEntries = GetNumQuestLogEntries; local QGet_QuestLogSelection = GetQuestLogSelection; local QSelect_QuestLogEntry = SelectQuestLogEntry; --------------------------------------------------------------------------------------------------- --Global Vars --------------------------------------------------------------------------------------------------- LastQuestLogHashes = nil; LastQuestLogCount = 0; lastObjectives = nil; QuestAbandonOnAccept = nil; QuestAbandonWithItemsOnAccept = nil; QuestRewardCompleteButton = nil; QuestProgressCompleteButton = nil; QuestDetailAcceptButton = nil; Questie.lastCollapsedCount = 0; Questie.collapsedThisRun = false; QUESTIE_LAST_UPDATECACHE = GetTime(); --------------------------------------------------------------------------------------------------- --Blizzard Hook: Quest Abandon On Accept --------------------------------------------------------------------------------------------------- QuestAbandonOnAccept = StaticPopupDialogs["ABANDON_QUEST"].OnAccept; StaticPopupDialogs["ABANDON_QUEST"].OnAccept = function() local qName = GetAbandonQuestName(); for hash,v in pairs(QuestieCachedQuests) do if v["questName"] == qName then QuestieSeenQuests[hash] = -1; QuestieCachedQuests[hash] = nil; QuestieHandledQuests[hash] = nil; --Questie:debug_Print("Quest:QuestAbandonOnAccept: [questTitle: "..qName.."] | [Hash: "..hash.."]"); RemoveCrazyArrow(hash); end end QuestAbandonOnAccept(); end --------------------------------------------------------------------------------------------------- --Blizzard Hook: Quest Abandon With Items On Accept --------------------------------------------------------------------------------------------------- QuestAbandonWithItemsOnAccept = StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"].OnAccept; StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"].OnAccept = function() local qName = GetAbandonQuestName(); for hash,v in pairs(QuestieCachedQuests) do if v["questName"] == qName then QuestieSeenQuests[hash] = -1; QuestieCachedQuests[hash] = nil; QuestieHandledQuests[hash] = nil; --Questie:debug_Print("Quest:QuestAbandonWithItemsOnAccept: [questTitle: "..qName.."] | [Hash: "..hash.."]"); RemoveCrazyArrow(hash); end end QuestAbandonWithItemsOnAccept(); end --------------------------------------------------------------------------------------------------- --This function saves the number of slots that get freed when turning in a quest --to the QuestieCachedQuests table, so it can be read in the next function. --It is called upon the QUEST_PROGRESS event. --------------------------------------------------------------------------------------------------- function Questie:OnQuestProgress() local questTitle = QGet_TitleText(); local _, _, qlevel, qName = string.find(questTitle, "%[(.+)%] (.+)"); if qName == nil then qName = QGet_TitleText(); end for hash,v in pairs(QuestieCachedQuests) do if v["questName"] == qName then v["numQuestItems"] = GetNumQuestItems(); end end end --------------------------------------------------------------------------------------------------- --This function is used by the next two hooks. --It checks if the conditions for completing a quest are met: -- 1. If there is a choice available, the player must have chosen. -- 2. There must be (numRewards-numItems) free slots in the invetory. --If both conditions are met, the quest is marked as finished, otherwise --false is returned (return value is currently unused). --------------------------------------------------------------------------------------------------- function Questie:MarkQuestAsFinished() local rewards = GetNumQuestRewards(); local choices = GetNumQuestChoices(); -- Filter condition: choices available but no choice made. if ( QuestFrameRewardPanel.itemChoice == 0 and choices > 0 ) then return false; end -- If there are rewards and choices, we need 1 more space. if choices > 0 then rewards = rewards + 1; end -- Get quest name and compare it against values in cache local questTitle = QGet_TitleText(); local _, _, qlevel, qName = string.find(questTitle, "%[(.+)%] (.+)"); if qName == nil then qName = QGet_TitleText(); end for hash,v in pairs(QuestieCachedQuests) do if v["questName"] == qName then if (not v["numQuestItems"]) then Questie:debug_Print("ERROR: QuestRewardCompleteButton: [questTitle: "..qName.."] | [Hash: "..hash.."]:\n Failed to read numQuestItems from SavedVariables") end -- Filter condition: not enough space in inventory. if (rewards > 0) and (Questie:CheckPlayerInventory() < (rewards - (v["numQuestItems"] or 0))) then return false; end -- All checks passed, mark quest as finished and remove it from cache QuestieSeenQuests[hash] = 1; QuestieCompletedQuestMessages[qName] = 1; QuestieCachedQuests[hash] = nil; QuestieHandledQuests[hash] = nil; Questie:debug_Print("Quest:QuestRewardCompleteButton: [questTitle: "..qName.."] | [Hash: "..hash.."]"); RemoveCrazyArrow(hash); return true; end end end --------------------------------------------------------------------------------------------------- --Blizzard Hook: GetQuestReward function --This hook marks a quest as finished in Questies DB if it can be finished. --The call to the hooked function then finishes the quest. --It is needed in addition to the next hook, because it is used by EQL3 --when its "Auto Complete Quests"-option is enabled. --------------------------------------------------------------------------------------------------- QGetQuestReward = GetQuestReward; GetQuestReward = function(choice) Questie:MarkQuestAsFinished(); QGetQuestReward(choice); end --------------------------------------------------------------------------------------------------- --Blizzard Hook: Quest Reward Complete Button --This hook marks a quest as finished in Questies DB if it can be finished. --The call to the hooked function then finishes the quest. --------------------------------------------------------------------------------------------------- QuestRewardCompleteButton = QuestRewardCompleteButton_OnClick; QuestRewardCompleteButton_OnClick = function() Questie:MarkQuestAsFinished(); QuestRewardCompleteButton(); end --------------------------------------------------------------------------------------------------- --Blizzard Hook: Quest Progress Accept Button --------------------------------------------------------------------------------------------------- QuestDetailAcceptButton = QuestDetailAcceptButton_OnClick; function QuestDetailAcceptButton_OnClick() Questie:CheckQuestLogStatus(); QuestDetailAcceptButton(); end --------------------------------------------------------------------------------------------------- --Matches a looted item to quest items that are contained in the QuestieCachedQuests table --------------------------------------------------------------------------------------------------- function Questie:DetectQuestItem(itemName) for k, v in pairs(QuestieCachedQuests) do local num = v["leaderboards"] for i=1,num do desc = v["objective"..i]["desc"] if (desc) then local _, _, questItem, itemHave, itemNeed = string.find(desc, "(.+)%: (%d+)/(%d+)"); if itemName == questItem and itemHave ~= itemNeed then --Questie:debug_Print("Quest:DetectQuestItem: TRUE"); --Questie:debug_Print("Quest:DetectQuestItem: [itemName: "..itemName.."] | [questItem: "..questItem.."] | [itemHave: "..itemHave.."] | [itemNeed: "..itemNeed.."]"); return true else --Questie:debug_Print("Quest:DetectQuestItem: FALSE"); return false end end end end end --------------------------------------------------------------------------------------------------- --Parses loot messages then passes item to DetectQuestItem for verification --------------------------------------------------------------------------------------------------- function Questie:ParseQuestLoot(arg1) local msg, item, loot if string.find(arg1, "(You receive loot%:) (.+)") then _, _, msg, item = string.find(arg1, "(You receive loot%:) (.+)"); elseif string.find(arg1, "(Received item%:) (.+)") then _, _, msg, item = string.find(arg1, "(Received item%:) (.+)"); elseif string.find(arg1, "(You receive item%:) (.+)") then _, _, msg, item = string.find(arg1, "(You receive item%:) (.+)"); end if item then _, _, loot = string.find(item, "%[(.+)%].+"); if Questie:DetectQuestItem(loot) then --Questie:debug_Print("Quest:ParseQuestLoot --> [POST] Quest Loot: [ "..loot.." ] was found."); Questie:CheckQuestLogStatus(); else --Questie:debug_Print("Quest:ParseQuestLoot --> [POST] Quest Loot: [ "..loot.." ] is not a quest item."); end end end --------------------------------------------------------------------------------------------------- --Used to make sure the players inventory isn't full before auto-completing quest. --------------------------------------------------------------------------------------------------- function Questie:CheckPlayerInventory() local totalSlots, usedSlosts, availableSlots; local totalSlots = 0; local usedSlots = 0; for bag = 0, 4 do local size = GetContainerNumSlots(bag); if (size and size > 0) then totalSlots = totalSlots + size; for slot = 1, size do if (GetContainerItemInfo(bag, slot)) then usedSlots = usedSlots + 1; end end end end availableSlots = totalSlots - usedSlots; return availableSlots end --------------------------------------------------------------------------------------------------- --Finishes a quest and performs a recrusive check to make sure all the required quests that come --before it are also finsihed and recorded in the players QuestieSeenQuests. It will also clear --any redundant quest tracking data and make sure a quest that is in a players log isn't --accidently marked finished. When ever this function is run it will also remove invalid tracker --data when it doesn't find a matching hash in the QuestieSeenQuests table. This sometimes --happens when a player starts a quest chain. --------------------------------------------------------------------------------------------------- function Questie:finishAndRecurse(questhash) local QSQ = QuestieSeenQuests; local QCQ = QuestieCachedQuests; local QHM = QuestieHashMap; --If it finds a completed quest with left over cached data, then the cached --data gets cleared. if (QSQ[questhash] == 1) then if (QCQ[questhash]) then QCQ[questhash] = nil; end end --This loop checks to make sure a quest is finished before marking it complete. It then --recursively checks all required quests before it and marks those as complete as well. It --also checks each one to make sure we aren't marking a seen quest finished. if (QSQ[questhash] == 0) and (QCQ[questhash]) then if ((QCQ[questhash]["leaderboards"] == 0 or QCQ[questhash]["leaderboards"] == 1) or (QCQ[questhash]["isComplete"] == 1)) then QSQ[questhash] = 1; QCQ[questhash] = nil; RemoveCrazyArrow(questhash); else local req = nil; if QHM[questhash] then req = QHM[questhash]['rq']; end if req and QSQ[req] ~= 1 then Questie:finishAndRecurse(req); end return; end --This loop allows a player to recursively finish a quest and all required quests that comes --before it by shift+clicking an icon from one of the maps. It also checks each one to make --sure we aren't marking a seen quest finished. elseif ((QSQ[questhash] == nil) and (QCQ[questhash] == nil)) then QSQ[questhash] = 1; local req = nil; if QHM[questhash] then req = QHM[questhash]['rq']; end if req and QSQ[req] ~= 1 then Questie:finishAndRecurse(req); else return; end end --This trolls through all cached data to make sure it stays cleaned up. local index = 0; for i,v in pairs(QCQ) do if QSQ[i] == 1 then QCQ[i] = nil; index = index + 1; end end end --------------------------------------------------------------------------------------------------- --Checks the players quest log upon login or ReloadUI to make sure QuestieMapNotes and --QuestieCachedQuests get pre-populated with cache data before normal CheckLog functions are run. --This is especially important if this data isn't already in the WoW game clients local cache. --------------------------------------------------------------------------------------------------- function Questie:UpdateGameClientCache(force) if (IsQuestieActive == false) then return; end Questie:debug_Print(); Questie:debug_Print("****************| Running Quest:UpdateGameClientCache |****************"); local prevQuestLogSelection = QGet_QuestLogSelection(); local id = 1; local qc = 0; local nEntry, nQuests = QGet_NumQuestLogEntries(); while qc < nQuests do local questName, level, _, isHeader, isCollapsed, _ = QGet_QuestLogTitle(id); if not isHeader and not isCollapsed then QSelect_QuestLogEntry(id); local questText, objectiveText = QGet_QuestLogQuestText(); local hash = Questie:getQuestHash(questName, level, objectiveText); if (force) then Questie:AddQuestToMap(hash, true); Questie:debug_Print("Quest:UpdateGameClientCache --> Questie:AddQuestToMap(forced): [Name: "..questName.."]"); QuestieTracker:addQuestToTrackerCache(hash, id, level); Questie:debug_Print("Quest:UpdateGameClientCache --> Questie:addQuestToTrackerCache(forced): [Hash: "..hash.."]"); end for index=1, QGet_NumQuestLeaderBoards(id) do local desc = QGet_QuestLogLeaderBoard(index, id); local objectiveName = desc; local splitIndex = findLast(objectiveName, ":"); if splitIndex ~= nil then objectiveName = string.sub(objectiveName, 1, splitIndex-1); if (string.find(objectiveName, " slain")) then objectiveName = string.sub(objectiveName, 1, string.len(objectiveName)-6); end end if (not LastQuestLogHashes and not force) or (QuestieHandledQuests[hash] and QuestieHandledQuests[hash]["objectives"] and QuestieHandledQuests[hash]["objectives"][index]["name"] ~= objectiveName) then Questie:AddQuestToMap(hash); Questie:debug_Print("Quest:UpdateGameClientCache --> Questie:AddQuestToMap(): [Name: "..QuestieHandledQuests[hash]["objectives"][index]["name"].."]"); QuestieTracker:addQuestToTrackerCache(hash, id, level); Questie:debug_Print("Quest:UpdateGameClientCache --> Questie:addQuestToTrackerCache(): [Hash: "..hash.."]"); end end end if not isHeader then qc = qc + 1; end id = id + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); end --------------------------------------------------------------------------------------------------- --Checks the players quest log --------------------------------------------------------------------------------------------------- function Questie:CheckQuestLog() --LastQuestLogHashes should always be nil upon Login or a ReloadUI - do these checks if (not LastQuestLogHashes) then Questie:debug_Print(); Questie:debug_Print("****************| Running [PRE] Quest:CheckQuestLog |****************"); --Clears abandoned quests for k, v in pairs(QuestieSeenQuests) do if (QuestieSeenQuests[k] == -1) then Questie:RemoveQuestFromMap(k); QuestieCachedQuests[k] = nil; QuestieSeenQuests[k] = nil; QUEST_WATCH_LIST[k] = nil; Questie:debug_Print("Quest:CheckQuestLog: Found abandoned quest in QuestDB - Removed: [Hash: "..k.."]"); end end --Clears cached data for k, v in pairs(QuestieCachedQuests) do if QuestieSeenQuests[k] == 1 then Questie:RemoveQuestFromMap(k); QuestieCachedQuests[k] = nil; Questie:debug_Print("Quest:CheckQuestLog: Found cached data for a finished quest - Removed: [Hash: "..k.."]"); end end LastQuestLogHashes, LastQuestLogCount = Questie:AstroGetAllCurrentQuestHashesAsMeta(); for k, v in pairs(LastQuestLogHashes) do --If a quest is found in the log and for some reason it's set as finished (1), or --missing all together (nil), reset its status back to active (0). if QuestieSeenQuests[k] == 1 or QuestieSeenQuests[k] == nil then QuestieSeenQuests[k] = 0; Questie:debug_Print("Quest:CheckQuestLog: --> Quest found in QuestLog marked complete - Fixed: [Hash: "..v["hash"].."]"); end --This "double-tap" ensures quest data is inserted into the cache if (QuestieCachedQuests[v["hash"]] == nil) or (QuestieHandledQuests[v["hash"]] == nil) then QuestieTracker:addQuestToTrackerCache(v["hash"], v["logId"], v["level"]); Questie:AddQuestToMap(v["hash"]); Questie:debug_Print("Quest:CheckQuestLog: --> Add quest to Tracker and MapNotes caches: [Hash: "..v["hash"].."]"); end end --Removes active quests from QuestDB if it's not active in the QuestLog for k, v in pairs(QuestieSeenQuests) do if QuestieSeenQuests[k] == 0 and LastQuestLogHashes[k] == nil then QuestieCachedQuests[k] = nil; QuestieSeenQuests[k] = nil; QUEST_WATCH_LIST[k] = nil; Questie:debug_Print("Quest:CheckQuestLog: --> Quest found in QuestDB not in QuestLog - Removed: [Hash: "..k.."]"); end end QUESTIE_LAST_UPDATE_FINISHED = GetTime(); return; end local CheckLogTime = GetTime(); local Quests, QuestsCount = Questie:AstroGetAllCurrentQuestHashesAsMeta(); MapChanged = false; delta = {}; if (QuestsCount > LastQuestLogCount) then for k, v in pairs(Quests) do if (Quests[k] and LastQuestLogHashes[k]) then else if (Quests[k]) then v["deltaType"] = 1; table.insert(delta, v); else v["deltaType"] = 0; table.insert(delta, v); end end end else for k, v in pairs(LastQuestLogHashes) do if (Quests[k] and LastQuestLogHashes[k]) then else if (Quests[k]) then v["deltaType"] = 1; table.insert(delta, v); else v["deltaType"] = 0; table.insert(delta, v); end end end end for k, v in pairs(delta) do Questie:debug_Print(); Questie:debug_Print("****************| Running [POST] Quest:CheckQuestLog |**************** "); Questie:debug_Print("Quest:CheckQuestLog: UPON ENTER: [QuestsCount: "..QuestsCount.."] | [LastCount: "..LastQuestLogCount.."]"); if (v["deltaType"] == 1) then Questie:AddQuestToMap(v["hash"]); --This adds a quest to the cache if (QuestieSeenQuests[v["hash"]] == nil) then QuestieSeenQuests[v["hash"]] = 0; QuestieTracker:addQuestToTrackerCache(v["hash"], v["logId"], v["level"]); Questie:debug_Print("Quest:CheckQuestLog: --> Add quest to Tracker and MapNotes caches: [Hash: "..v["hash"].."]"); RemoveCrazyArrow(v["hash"]); if (AUTO_QUEST_WATCH == "1") then AddQuestWatch(v["logId"]); end end MapChanged = true; elseif not Questie.collapsedThisRun then Questie:RemoveQuestFromMap(v["hash"]); --This clears cache of finished quests if (QuestieSeenQuests[v["hash"]] == 1) then QuestieTracker:removeQuestFromTracker(v["hash"]); QUEST_WATCH_LIST[v["hash"]] = nil; Questie:finishAndRecurse(v["hash"]); Questie:debug_Print("Quest:CheckQuestLog: --> Quest:finishAndRecurse() [Hash: "..v["hash"].."]"); if (not QuestieCompletedQuestMessages[v["name"]]) then QuestieCompletedQuestMessages[v["name"]] = 0; end --This clears cache of abandoned quests elseif (QuestieSeenQuests[v["hash"]] == -1) then QuestieTracker:removeQuestFromTracker(v["hash"]); QuestieCachedQuests[v["hash"]] = nil; QuestieSeenQuests[v["hash"]] = nil; QUEST_WATCH_LIST[v["hash"]] = nil; Questie:debug_Print("Quest:CheckQuestLog: clear abandoned quest: [Hash: "..v["hash"].."]"); end --Cleans cached data for k, v in pairs(QuestieCachedQuests) do if QuestieSeenQuests[k] == 1 then QuestieCachedQuests[k] = nil; Questie:debug_Print("Quest:CheckQuestLog: Cleaned Quest Cache: [Hash: "..k.."]"); end end if lastObjectives and lastObjectives[v["hash"]] then Questie:debug_Print("Quest:CheckQuestLog: lastObjectives update [Hash: "..v["hash"].."]"); lastObjectives = {}; end MapChanged = true; end end delta = nil; LastQuestLogHashes = Quests; LastQuestLogCount = QuestsCount; if (MapChanged == true) then Questie:debug_Print("Quest:CheckQuestLog: QuestLog Changed --> Questie:RefreshQuestStatus()"); Questie:RefreshQuestStatus(); QUESTIE_LAST_UPDATE_FINISHED = GetTime(); Questie:debug_Print("Quest:CheckQuestLog: UPON EXIT: [QuestsCount: "..QuestsCount.."] | [LastCount: "..LastQuestLogCount.."]"); return true; else Questie:debug_Print("Quest:CheckQuestLog: NO CHANGE --> Refresh Notes and Tracker"); Questie:AddEvent("SYNCLOG", 0.2); Questie:AddEvent("DRAWNOTES", 0.4); Questie:AddEvent("TRACKER", 0.6); QUESTIE_LAST_UPDATE_FINISHED = GetTime(); return nil; end end --------------------------------------------------------------------------------------------------- --Adds or updates all active objectives in the questlog to the lastObjectives table --------------------------------------------------------------------------------------------------- function Questie:UpdateQuests(force) if (not lastObjectives) then lastObjectives = {}; Questie:UpdateQuestsInit(); return; end local UpdateQuestsTime = GetTime(); local ZonesChecked = 0; local CurrentZone = GetZoneText(); local numEntries, numQuests = QGet_NumQuestLogEntries(); local change = Questie:UpdateQuestInZone(CurrentZone); local i = 1; local qc = 0; ZonesChecked = ZonesChecked + 1; if (not change) then change = Questie:UpdateQuestInZone(GetMinimapZoneText()); ZonesChecked = ZonesChecked + 1; end if (not change or force) then while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if (isHeader and q ~= CurrentZone) then local c = Questie:UpdateQuestInZone(q, force); ZonesChecked = ZonesChecked + 1; change = c; if (c and not force)then break; end end if not isHeader then qc = qc + 1; end i = i + 1; end else end return change; end --------------------------------------------------------------------------------------------------- --Updates all active objectives in a zone then updates the lastObjectives table --------------------------------------------------------------------------------------------------- function Questie:UpdateQuestInZone(Zone, force) local numEntries, numQuests = QGet_NumQuestLogEntries(); local foundChange = nil; local ZoneFound = nil; local QuestsChecked = 0; local i = 1; local qc = 0; local prevQuestLogSelection = QGet_QuestLogSelection(); while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if (ZoneFound and isHeader) then break; end if (isHeader and q == Zone) then ZoneFound = true; end if not isHeader and ZoneFound then QuestsChecked = QuestsChecked + 1; QSelect_QuestLogEntry(i); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local hash = Questie:getQuestHash(q, level, objectiveText); if QuestieHashCache[q] == nil then QuestieHashCache[q] = {}; end QuestieHashCache[q][hash] = GetTime(); if not lastObjectives[hash] then lastObjectives[hash] = {}; end local Refresh = nil; for obj = 1, count do if (not lastObjectives[hash][obj]) then lastObjectives[hash][obj] = {}; end local desc, typ, done = QGet_QuestLogLeaderBoard(obj); if(lastObjectives[hash][obj].desc == desc and lastObjectives[hash][obj].typ == typ and lastObjectives[hash][obj].done == done) then elseif(lastObjectives[hash][obj].done ~= done) then Refresh = true; foundChange = true; else foundChange = true; end lastObjectives[hash][obj].desc = desc; lastObjectives[hash][obj].typ = typ; lastObjectives[hash][obj].done = done; end if (Refresh) then Questie:AddQuestToMap(hash, true); end if (foundChange and QuestieConfig.trackerEnabled == true) then if (QuestieCachedQuests[hash]) then QuestieTracker:updateTrackerCache(hash, i, level); end end end if (foundChange and not force) then break; end if not isHeader then qc = qc + 1; end i = i + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); return foundChange; end --------------------------------------------------------------------------------------------------- --Adds all active objectives from all quests in the questlog to the lastObjectives table --------------------------------------------------------------------------------------------------- function Questie:UpdateQuestsInit() local numEntries, numQuests = QGet_NumQuestLogEntries(); local i = 1; local qc = 0; local prevQuestLogSelection = QGet_QuestLogSelection(); while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if not isHeader then QSelect_QuestLogEntry(i); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local hash = Questie:getQuestHash(q, level, objectiveText); if not lastObjectives[hash] then lastObjectives[hash] = {}; end for obj = 1, count do if (not lastObjectives[hash][obj]) then lastObjectives[hash][obj] = {}; end lastObjectives[hash][obj].desc = desc; lastObjectives[hash][obj].typ = typ; lastObjectives[hash][obj].done = done; end qc = qc + 1; end i = i + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); end --------------------------------------------------------------------------------------------------- --Astrolabe functions --------------------------------------------------------------------------------------------------- function Questie:AstroGetAllCurrentQuestHashes(print) local hashes = {}; local numEntries, numQuests = QGet_NumQuestLogEntries(); local i = 1; local qc = 0; if (print) then --Questie:debug_Print("Quest:AstroGetAllCurrentQuestHashes: Listing all current quests"); end local prevQuestLogSelection = QGet_QuestLogSelection(); while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if not isHeader then QSelect_QuestLogEntry(i); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local quest = {}; quest["name"] = q; quest["level"] = level; local hash = Questie:getQuestHash(q, level, objectiveText); quest["hash"] = hash; if(IsAddOnLoaded("URLCopy") and print) then Questie:debug_Print(" "..q,URLCopy_Link(quest["hash"])); elseif(print) then Questie:debug_Print(" "..q,quest["hash"]); end table.insert(hashes, quest); qc = qc + 1; else if (print) then Questie:debug_Print(" Zone:", q); end end i = i + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); if (print) then --Questie:debug_Print("Quest:AstroGetAllCurrentQuestHashes: End of all current quests"); end return hashes; end --------------------------------------------------------------------------------------------------- function Questie:AstroGetAllCurrentQuestHashesAsMeta(print) local agacqhamtime = GetTime(); local hashes = {}; local Count = 0; local numEntries, numQuests = QGet_NumQuestLogEntries(); local collapsedCount = 0; local i = 1; local qc = 0; Questie.collapsedThisRun = false; local prevQuestLogSelection = QGet_QuestLogSelection(); while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if isCollapsed then collapsedCount = collapsedCount + 1; end if not isHeader then QSelect_QuestLogEntry(i); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local hash = Questie:getQuestHash(q, level, objectiveText); if hash >= 0 then hashes[hash] = {}; hashes[hash]["hash"] = hash; hashes[hash]["name"] = q; hashes[hash]["level"] = level; hashes[hash]["logId"] = i; if(IsAddOnLoaded("URLCopy") and print)then Questie:debug_Print(" "..q,URLCopy_Link(quest["hash"])); elseif(print) then Questie:debug_Print(" "..q,quest["hash"]); end end qc = qc + 1; else if (print) then Questie:debug_Print(" Zone:", q); end end i=i+1 end QSelect_QuestLogEntry(prevQuestLogSelection); if (print) then --Questie:debug_Print("Quest:AstroGetAllCurrentQuestHashesAsMeta: End of all current quests"); end if not (collapsedCount == Questie.lastCollapsedCount) then Questie.lastCollapsedCount = collapsedCount; Questie.collapsedThisRun = true; end --Questie:debug_Print("Quest:AstroGetAllCurrentQuestHashesAsMeta --> Getting all hashes took: ["..tostring((GetTime()- agacqhamtime)*1000).."ms]"); return hashes, numQuests; end --------------------------------------------------------------------------------------------------- function Questie:AstroGetFinishedQuests() numEntries, numQuests = QGet_NumQuestLogEntries(); local FinishedQuests = {}; local i = 1; local qc = 0; local prevQuestLogSelection = QGet_QuestLogSelection(); while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if not isHeader then QSelect_QuestLogEntry(i); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); Done = true; for obj = 1, count do local desc, typ, done = QGet_QuestLogLeaderBoard(obj); if not done then Done = nil; end end if(Done) then local hash = Questie:getQuestHash(q, level, objectiveText); --Questie:debug_Print("AstroGetFinishedQuests: [Hash: "..hash.."] | [Quest: "..q.."] | [Level: "..level.."]"); table.insert(FinishedQuests, hash); end qc = qc + 1; end i = i + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); return FinishedQuests; end --------------------------------------------------------------------------------------------------- function Questie:GetQuestObjectivePaths(questHash) local prevQuestLogSelection = QGet_QuestLogSelection(); local questLogID = Questie:GetQuestIdFromHash(questHash); QSelect_QuestLogEntry(questLogID); local count = QGet_NumQuestLeaderBoards(); local objectivePaths = {}; for i = 1, count do local desc, type, done = QGet_QuestLogLeaderBoard(i); local typeFunctions = { ['item'] = GetItemLocations, ['event'] = GetEventLocations, ['monster'] = GetMonsterLocations, ['object'] = GetObjectLocations, ['reputation'] = GetReputationLocations }; local typeFunction = typeFunctions[type]; if typeFunction ~= nil then local objectiveName = desc; local splitIndex = findLast(objectiveName, ":"); if splitIndex ~= nil then objectiveName = string.sub(objectiveName, 1, splitIndex-1); if (string.find(objectiveName, " slain")) then objectiveName = string.sub(objectiveName, 1, string.len(objectiveName)-6); end end locations = typeFunction(objectiveName); objectivePaths[i] = {}; objectivePaths[i]['path'] = locations; objectivePaths[i]['done'] = done; objectivePaths[i]['type'] = type; objectivePaths[i]['name'] = objectiveName; objectivePaths[i]['desc'] = desc end end QSelect_QuestLogEntry(prevQuestLogSelection); return objectivePaths; end --------------------------------------------------------------------------------------------------- --Perhaps we should consider removing this function from Questie --------------------------------------------------------------------------------------------------- function Questie:AstroGetQuestObjectives(questHash) local prevQuestLogSelection = QGet_QuestLogSelection(); local QuestLogID = Questie:GetQuestIdFromHash(questHash); local mapid = GetCurrentMapID(); local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(QuestLogID); QSelect_QuestLogEntry(QuestLogID); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local AllObjectives = {}; AllObjectives["QuestName"] = q; AllObjectives["objectives"] = {}; for i = 1, count do local desc, typ, done = QGet_QuestLogLeaderBoard(i); local typeFunction = AstroobjectiveProcessors[typ]; if typ == "item" or typ == "monster" or not (typeFunction == nil) then local indx = findLast(desc, ":"); local countless = indx == nil; local countstr = ""; local namestr = desc; if not countless then countstr = string.sub(desc, indx + 2); namestr = string.sub(desc, 1, indx - 1); end local objectives = typeFunction(q, namestr, countstr, selected, mapid); Objective = {}; local hash = Questie:getQuestHash(q, level, objectiveText); for k, v in pairs(objectives) do if (AllObjectives["objectives"][v["name"]] == nil) then AllObjectives["objectives"][v["name"]] = {}; end if (not QuestieCachedMonstersAndObjects[hash]) then QuestieCachedMonstersAndObjects[hash] = {}; end if (not QuestieCachedMonstersAndObjects[hash][v["name"]]) then QuestieCachedMonstersAndObjects[hash][v["name"]] = {}; end QuestieCachedMonstersAndObjects[hash][v["name"]].name = v["name"]; for monster, info in pairs(v['locations']) do local obj = {}; obj["mapid"] = info[1]; obj["x"] = info[2]; obj["y"] = info[3]; obj["lootname"] = v["lootname"]; obj["type"] = v["type"]; obj["done"] = done; obj['objectiveid'] = i; table.insert(AllObjectives["objectives"][v["name"]], obj); end end else end end QSelect_QuestLogEntry(prevQuestLogSelection); return AllObjectives; end --------------------------------------------------------------------------------------------------- AstroobjectiveProcessors = { ['item'] = function(quest, name, amount, selected, mapid) local list = {}; local itemdata = QuestieItems[name]; --Questie:debug_Print(name); if itemdata == nil then Questie:debug_Print("Quest:AstroobjectiveProcessors --> ERROR1 PROCESSING: [Quest: "..quest.."] | [Objective: "..name.."] | No [itemdata] found | ID:0"); itemdata = QuestieItems[name]; end if itemdata then for k,v in pairs(itemdata) do if k == "locationCount" then local monster = {}; monster["name"] = name; monster["locations"] = {}; monster["type"] = "loot"; for b=1,itemdata['locationCount'] do local loc = itemdata['locations'][b]; table.insert(monster["locations"], loc); end table.insert(list, monster); elseif k == "drop" then for e,r in pairs(v) do local monster = {}; monster["name"] = name; monster["lootname"] = e; monster["locations"] = {}; monster["type"] = "loot"; for k, pos in pairs(QuestieMonsters[e]['locations']) do table.insert(monster["locations"], pos); end table.insert(list, monster); end elseif k == "contained" then for objectName, someNumber in pairs(v) do local monster = {}; monster["name"] = name; monster["lootname"] = objectName; monster["locations"] = {}; monster["type"] = "object"; if QuestieObjects[objectName] then --TODO: handle objects that appear when a mob is killed for k, pos in pairs(QuestieObjects[objectName]['locations']) do table.insert(monster["locations"], pos); end table.insert(list, monster); end end elseif k =="locations" then else Questie:debug_Print("Quest:AstroobjectiveProcessors --> ERROR2: [Quest: "..quest.."] | [Objective: "..name.."] | ID:1"); for s, r in pairs(itemdata) do Questie:debug_Print(s,tostring(r)); end end end end return list; end, ['event'] = function(quest, name, amount, selected, mapid) local evtdata = QuestieEvents[name]; local list = {}; if evtdata == nil then Questie:debug_Print("Quest:AstroobjectiveProcessors --> ERROR3 UNKNOWN EVENT: [Quest: "..quest.."] | [Objective: "..name.."] | ID:2"); else for b=1,evtdata['locationCount'] do local monster = {}; monster["name"] = name; monster["locations"] = {}; monster["type"] = "event"; for b=1,evtdata['locationCount'] do local loc = evtdata['locations'][b]; table.insert(monster["locations"], loc); end table.insert(list, monster); end end return list; end, ['monster'] = function(quest, name, amount, selected, mapid) local list = {}; local monster = {}; if (string.find(name, " slain")) then name = string.sub(name, 1, string.len(name)-6); end monster["name"] = name; monster["type"] = "slay"; monster["locations"] = {}; if (QuestieMonsters[name] and QuestieMonsters[name]['locations']) then for k, pos in pairs(QuestieMonsters[name]['locations']) do table.insert(monster["locations"], pos); end end table.insert(list, monster); return list; end, ['object'] = function(quest, name, amount, selected, mapid) local list = {}; local objdata = QuestieObjects[name]; if objdata == nil then Questie:debug_Print("Quest:AstroobjectiveProcessors: ERROR4 UNKNOWN OBJECT: [Quest: "..quest.."] | [Objective: "..name.."]"); else for b=1,objdata['locationCount'] do local monster = {}; monster["name"] = name; monster["locations"] = {}; monster["type"] = "object"; for b=1,objdata['locationCount'] do local loc = objdata['locations'][b]; table.insert(monster["locations"], loc); end table.insert(list, monster); end end return list; end } --------------------------------------------------------------------------------------------------- --End of Astrolabe functions --------------------------------------------------------------------------------------------------- --///////////////////////////////////////////////////////////////////////////////////////////////-- --------------------------------------------------------------------------------------------------- --Get quest ID from quest hash --------------------------------------------------------------------------------------------------- function Questie:GetQuestIdFromHash(questHash) local numEntries, numQuests = QGet_NumQuestLogEntries(); if (QUESTIE_UPDATE_EVENT or numEntries ~= LastNrOfEntries or not CachedIds[questHash]) then CachedIds[questHash] = {}; QUESTIE_UPDATE_EVENT = 0; LastNrOfEntries = numEntries; Questie:UpdateQuestIds(); if CachedIds[questHash] then return CachedIds[questHash]; end else local prevQuestLogSelection = QGet_QuestLogSelection(); local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(CachedIds[questHash]); QSelect_QuestLogEntry(CachedIds[questHash]); local questText, objectiveText = QGet_QuestLogQuestText(); if (q and level and objectiveText) then if(Questie:getQuestHash(q, level, objectiveText) == questHash) then QSelect_QuestLogEntry(prevQuestLogSelection) return CachedIds[questHash]; else Questie:debug_Print("Quest:GetQuestIdFromHash --> Error: [Hash: "..tostring(CachedIds[questHash]).."]1"); end else Questie:debug_Print("Quest:GetQuestIdFromHash --> Error2: [Hash: "..tostring(CachedIds[questHash]).."] | [Quest: "..tostring(q).."] | [Level: "..tostring(level).."]"); end QSelect_QuestLogEntry(prevQuestLogSelection); end end --------------------------------------------------------------------------------------------------- --Update quest ID's --------------------------------------------------------------------------------------------------- function Questie:UpdateQuestIds() local uqidtime = GetTime() local numEntries, numQuests = QGet_NumQuestLogEntries(); local i = 1; local qc = 0; local prevQuestLogSelection = QGet_QuestLogSelection() while qc < numQuests do local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(i); if not isHeader then QSelect_QuestLogEntry(i); local questText, objectiveText = QGet_QuestLogQuestText(); local hash = Questie:getQuestHash(q, level, objectiveText); if (not q or not level or not objective) then --commented out the error because it was really annoying. -ZoeyZolotova --Questie:debug_Print("Quest:UpdateQuestIds --> Error1: [Name: "..tostring(name).."] | [Level: "..tostring(level).."] | [Id: "..tostring(i).."] | [Hash: "..tostring(hash).."]") end CachedIds[hash] = i; qc = qc + 1; end i = i + 1; end QSelect_QuestLogEntry(prevQuestLogSelection); --Questie:debug_Print("Quest:UpdateQuestID: --> Updating QuestIds took: ["..tostring((GetTime()- uqidtime)*1000).."ms]") end --------------------------------------------------------------------------------------------------- --Some outdated server databases still use names like "Tower of Althalaxx part x". --which were used to turn quest names into a unique key. This function removes --those suffixes, so that they don't harm the quest lookup. --------------------------------------------------------------------------------------------------- function Questie:SanitisedQuestLookup(name) local realName, matched = string.gsub(name, " [(]?[Pp]art %d+[)]?", ""); return QuestieLevLookup[realName] or false; end --------------------------------------------------------------------------------------------------- --Remove unique suffix from text. --------------------------------------------------------------------------------------------------- function Questie:RemoveUniqueSuffix(text) if string.sub(text, -1) == "]" then local strlen = string.len(text) text = string.sub(text, 1, strlen-4) end return text end --------------------------------------------------------------------------------------------------- --Lookup quest hash from name, level or objective text --------------------------------------------------------------------------------------------------- function Questie:getQuestHash(name, level, objectiveText) local hashLevel = level or "hashLevel"; local hashText = objectiveText or "hashText"; if QuestieQuestHashCache[name..hashLevel..hashText] then return QuestieQuestHashCache[name..hashLevel..hashText]; end local questLookup = Questie:SanitisedQuestLookup(name); local hasOthers = false; if questLookup then local count = 0; local retval = 0; local bestDistance = 4294967295; --some high number (0xFFFFFFFF) local race = UnitRace("Player"); for k,v in pairs(questLookup) do if QuestieHashMap[v[2]] ~= nil then local rr = v[1]; local adjustedDescription = Questie:RemoveUniqueSuffix(k) if count == 1 then hasOthers = true; end local requiredQuest = QuestieHashMap[v[2]]['rq'] if adjustedDescription == objectiveText and tonumber(QuestieHashMap[v[2]]['questLevel']) == hashLevel and checkRequirements(null, race, null, rr) and (not requiredQuest or QuestieSeenQuests[requiredQuest]) and not QuestieSeenQuests[v[2]] then QuestieQuestHashCache[name..hashLevel..hashText] = v[2]; return v[2],hasOthers; --exact match end local dist = 4294967294; if not (objectiveText == nil) then dist = Questie:Levenshtein(objectiveText, adjustedDescription); end if dist < bestDistance then bestDistance = dist; retval = v[2]; end count = count + 1; else Questie:debug_Print("ERROR: Quest '"..name.."' was found but data is missing for hash "..v[2].." Please report this on Github!") end end if not (retval == 0) then QuestieQuestHashCache[name..hashLevel..hashText] = retval; return retval, hasOthers; --nearest match end end if name == nil then return -1; end local hash = Questie:MixString(0, name); if not (level == nil) then hash = Questie:MixInt(hash, level); QuestieQuestHashCache[name..hashLevel..hashText] = hash; end if not (objectiveText == nil) then hash = Questie:MixString(hash, objectiveText); QuestieQuestHashCache[name..hashLevel..hashText] = hash; end QuestieQuestHashCache[name..hashLevel..hashText] = hash; return hash, false; end --------------------------------------------------------------------------------------------------- --Checks to see if a quest is finished by quest hash --------------------------------------------------------------------------------------------------- function Questie:IsQuestFinished(questHash) local id = Questie:GetQuestIdFromHash(questHash); if (not id) then return false; end local prevQuestLogSelection = QGet_QuestLogSelection() local FinishedQuests = {}; local q, level, questTag, isHeader, isCollapsed, isComplete = QGet_QuestLogTitle(id); QSelect_QuestLogEntry(id); local count = QGet_NumQuestLeaderBoards(); local questText, objectiveText = QGet_QuestLogQuestText(); local Done = true; for obj = 1, count do local desc, typ, done = QGet_QuestLogLeaderBoard(obj); if not done then Done = nil; end end QSelect_QuestLogEntry(prevQuestLogSelection); if (Done and Questie:getQuestHash(q, level, objectiveText) == questHash) then local ret = {}; ret["questHash"] = questHash; ret["name"] = q; ret["level"] = level; return ret; end return nil; end --------------------------------------------------------------------------------------------------- --Race, Class and Profession filter functions --------------------------------------------------------------------------------------------------- RaceBitIndexTable = { ['human'] = 1, ['orc'] = 2, ['dwarf'] = 3, ['nightelf'] = 4, ['night elf'] = 4, ['scourge'] = 5, ['undead'] = 5, ['tauren'] = 6, ['gnome'] = 7, ['troll'] = 8, ['goblin'] = 9 }; ClassBitIndexTable = { ['warrior'] = 1, ['paladin'] = 2, ['hunter'] = 3, ['rogue'] = 4, ['priest'] = 5, ['shaman'] = 7, ['mage'] = 8, ['warlock'] = 9, ['druid'] = 11 }; --------------------------------------------------------------------------------------------------- function unpackBinary(val) ret = {}; for q=0,16 do if bit.band(bit.rshift(val,q), 1) == 1 then table.insert(ret, true); else table.insert(ret, false); end end return ret; end --------------------------------------------------------------------------------------------------- function checkRequirements(class, race, dbClass, dbRace) local valid = true; if race and dbRace and not (dbRace == 0) then local racemap = unpackBinary(dbRace); valid = racemap[RaceBitIndexTable[strlower(race)]]; end if class and dbClass and valid and not (dbRace == 0)then local classmap = unpackBinary(dbClass); valid = classmap[ClassBitIndexTable[strlower(class)]]; end return valid; end --------------------------------------------------------------------------------------------------- function Questie:GetAvailableQuestHashes(mapFileName, levelFrom, levelTo) local mapid = -1; if(QuestieZones[mapFileName]) then c = QuestieZones[mapFileName][4]; z = QuestieZones[mapFileName][5]; end local class = UnitClass("Player"); local race = UnitRace("Player"); local hashes = {}; for l = 0,100 do if QuestieZoneLevelMap[c] and QuestieZoneLevelMap[c][z] then local content = QuestieZoneLevelMap[c][z][l]; if content then for v, locationMeta in pairs(content) do local qdata = QuestieHashMap[v]; if (qdata) then local stop = false; local questLevel = qdata.questLevel; for x in string.gfind(questLevel, "%d+") do questLevel = x; end questLevel = tonumber(questLevel); if QuestieConfig.minLevelFilter and questLevel < levelFrom then stop = true; end if QuestieConfig.maxLevelFilter and qdata.level > levelTo then stop = true; end if (not stop) then local requiredQuest = qdata['rq']; local requiredRaces = qdata['rr']; local requiredClasses = qdata['rc']; local requiredSkill = qdata['rs']; local valid = not QuestieSeenQuests[requiredQuest]; if(requiredQuest) then valid = QuestieSeenQuests[requiredQuest]; end valid = valid and (requiredSkill == nil or QuestieConfig.showProfessionQuests); if valid then valid = valid and checkRequirements(class, race, requiredClasses,requiredRaces); end if valid and not QuestieHandledQuests[requiredQuest] and not QuestieSeenQuests[v] then hashes[v] = locationMeta; end end end end end end end return hashes; end --------------------------------------------------------------------------------------------------- --End of filter functions ---------------------------------------------------------------------------------------------------