--[[ *** DataStore_Quests *** Written by : Thaoky, EU-Marécages de Zangar July 8th, 2009 --]] if not DataStore then return end local addonName = "DataStore_Quests" _G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0") local addon = _G[addonName] local THIS_ACCOUNT = "Default" local AddonDB_Defaults = { global = { Options = { TrackTurnIns = 1, -- by default, save the ids of completed quests in the history AutoUpdateHistory = 1, -- if history has been queried at least once, auto update it at logon (fast operation - already in the game's cache) }, Characters = { ['*'] = { -- ["Account.Realm.Name"] lastUpdate = nil, Quests = {}, QuestLinks = {}, Rewards = {}, History = {}, -- a list of completed quests, hash table ( [questID] = true ) HistoryBuild = nil, -- build version under which the history has been saved HistorySize = 0, HistoryLastUpdate = nil, } } } } -- *** Utility functions *** local function GetOption(option) return addon.db.global.Options[option] end local function GetQuestLogIndexByName(name) -- helper function taken from QuestGuru for i = 1, GetNumQuestLogEntries() do local title = GetQuestLogTitle(i); if title == strtrim(name) then return i end end end -- *** Scanning functions *** local headersState = {} local function SaveHeaders() local headerCount = 0 -- use a counter to avoid being bound to header names, which might not be unique. for i = GetNumQuestLogEntries(), 1, -1 do -- 1st pass, expand all categories local _, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i) if isHeader then headerCount = headerCount + 1 if isCollapsed then ExpandQuestHeader(i) headersState[headerCount] = true end end end end local function RestoreHeaders() local headerCount = 0 for i = GetNumQuestLogEntries(), 1, -1 do local _, _, _, _, isHeader = GetQuestLogTitle(i) if isHeader then headerCount = headerCount + 1 if headersState[headerCount] then CollapseQuestHeader(i) end end end wipe(headersState) end local REWARD_TYPE_CHOICE = "c" local REWARD_TYPE_REWARD = "r" local REWARD_TYPE_SPELL = "s" local function ScanQuests() local char = addon.ThisCharacter local quests = char.Quests local links = char.QuestLinks local rewards = char.Rewards wipe(quests) wipe(links) wipe(rewards) local currentSelection = GetQuestLogSelection() -- save the currently selected quest SaveHeaders() local RewardsCache = {} for i = 1, GetNumQuestLogEntries() do local title, _, questTag, groupSize, isHeader, _, isComplete = GetQuestLogTitle(i); if isHeader then quests[i] = "0|" .. (title or "") else SelectQuestLogEntry(i) local money = GetQuestLogRewardMoney() quests[i] = format("1|%s|%d|%d|%d", questTag or "", groupSize, money, isComplete or 0) links[i] = GetQuestLink(i) wipe(RewardsCache) local num = GetNumQuestLogChoices() -- these are the actual item choices proposed to the player if num > 0 then for i = 1, num do local _, _, numItems, _, isUsable = GetQuestLogChoiceInfo(i) local link = GetQuestLogItemLink("choice", i) if link then local id = tonumber(link:match("item:(%d+)")) if id then table.insert(RewardsCache, REWARD_TYPE_CHOICE .."|"..id.."|"..numItems.."|"..(isUsable or 0)) end end end end num = GetNumQuestLogRewards() -- these are the rewards given anyway if num > 0 then for i = 1, num do local _, _, numItems, _, isUsable = GetQuestLogRewardInfo(i) local link = GetQuestLogItemLink("reward", i) if link then local id = tonumber(link:match("item:(%d+)")) if id then table.insert(RewardsCache, REWARD_TYPE_REWARD .. "|"..id.."|"..numItems.."|"..(isUsable or 0)) end end end end if GetQuestLogRewardSpell() then -- apparently, there is only one spell as reward local _, _, isTradeskillSpell, isSpellLearned = GetQuestLogRewardSpell() if isTradeskillSpell or isSpellLearned then local link = GetQuestLogSpellLink() if link then local id = tonumber(link:match("spell:(%d+)")) if id then table.insert(RewardsCache, REWARD_TYPE_SPELL .. "|"..id) end end end end if #RewardsCache > 0 then rewards[i] = table.concat(RewardsCache, ",") end end end RestoreHeaders() SelectQuestLogEntry(currentSelection) -- restore the selection to match the cursor, must be properly set if a user abandons a quest addon.ThisCharacter.lastUpdate = time() end local function RefreshQuestHistory() -- called 5 seconds after login, if the current character already has an history, one that was saved in the same build, then it's safe to refresh it automatically local thisChar = addon.ThisCharacter if not thisChar.HistoryLastUpdate then return end -- never scanned the history before ? exit local _, version = GetBuildInfo() if version and thisChar.HistoryBuild and version == thisChar.HistoryBuild then -- proceed if version is the same as the one saved in the db QueryQuestsCompleted() end end -- *** Event Handlers *** local function OnPlayerAlive() -- print("DataStore_Quests.lua") -- DEBUG 2025 07 21 if addon.coaScannedThisSession then return end -- CoA: scan once at login (was ghost-gated, so data never saved on a normal login - the cause of "data not saved") addon.coaScannedThisSession = true ScanQuests() end local function OnQuestLogUpdate() addon:UnregisterEvent("QUEST_LOG_UPDATE") -- .. and unregister it right away, since we only want it to be processed once (and it's triggered way too often otherwise) ScanQuests() end local function OnUnitQuestLogChanged() -- triggered when accepting/validating a quest .. but too soon to refresh data addon:RegisterEvent("QUEST_LOG_UPDATE", OnQuestLogUpdate) -- so register for this one .. end local lastCompleteQuestLink local function OnQuestComplete() if GetOption("TrackTurnIns") ~= 1 then return end -- "QUEST_COMPLETE" is triggered when the UI reaches the page where a player can click on the "Complete" Button. -- At this point, only detect which quest we're dealing with, and save its link local num = GetQuestLogIndexByName(GetTitleText()); if num then lastCompleteQuestLink = GetQuestLink(num) -- or save quest id end end local queryVerbose local function OnQuestQueryComplete() local thisChar = addon.ThisCharacter local history = thisChar.History local quests = {} GetQuestsCompleted(quests) local count = 0 for questID in pairs(quests) do history[questID] = true count = count + 1 end local _, version = GetBuildInfo() -- save the current build, to know if we can requery and expect immediate execution thisChar.HistoryBuild = version thisChar.HistorySize = count thisChar.HistoryLastUpdate = time() if queryVerbose then addon:Print("Quest history successfully retrieved!") queryVerbose = nil end end -- ** Mixins ** local function _GetQuestLogSize(character) return #character.Quests end local function _GetQuestLogInfo(character, index) local quest = character.Quests[index] local link = character.QuestLinks[index] local isHeader, questTag, groupSize, money, isComplete = strsplit("|", quest) if isHeader == "0" then return true, questTag -- questTag contains the title in a header line end isComplete = tonumber(isComplete) return nil, link, questTag, tonumber(groupSize), tonumber(money), tonumber(isComplete) end local function _GetQuestLogNumRewards(character, index) local reward = character.Rewards[index] if reward then return select(2, gsub(reward, ",", ",")) + 1 -- returns the number of rewards (=count of ^ +1) end return 0 end local function _GetQuestLogRewardInfo(character, index, rewardIndex) local reward = character.Rewards[index] if not reward then return end local i = 1 for v in reward:gmatch("([^,]+)") do if rewardIndex == i then local rewardType, id, numItems, isUsable = strsplit("|", v) numItems = tonumber(numItems) or 0 isUsable = (isUsable and isUsable == 1) and true or nil return rewardType, tonumber(id), numItems, isUsable end i = i + 1 end end local function _GetQuestInfo(link) -- CoA: GetQuestLogInfo can hand back a nil link for a partial-data alt; degrade to nil -- returns instead of asserting (callers already nil-check the returned name/level). if type(link) ~= "string" then return end local questID, questLevel = link:match("quest:(%d+):(-?%d+)") local questName = link:match("%[(.+)%]") return questName, tonumber(questID), tonumber(questLevel) end local function _QueryQuestHistory() QueryQuestsCompleted() -- this call triggers "QUEST_QUERY_COMPLETE" queryVerbose = true end local function _GetQuestHistory(character) return character.History end local function _GetQuestHistoryInfo(character) -- return the size of the history, the timestamp, and the build under which it was saved return character.HistorySize, character.HistoryLastUpdate, character.HistoryBuild end local function _IsQuestCompletedBy(character, questID) return character.History[questID] -- nil = not completed (not in the table), true = completed end local PublicMethods = { GetQuestLogSize = _GetQuestLogSize, GetQuestLogInfo = _GetQuestLogInfo, GetQuestLogNumRewards = _GetQuestLogNumRewards, GetQuestLogRewardInfo = _GetQuestLogRewardInfo, GetQuestInfo = _GetQuestInfo, QueryQuestHistory = _QueryQuestHistory, GetQuestHistory = _GetQuestHistory, GetQuestHistoryInfo = _GetQuestHistoryInfo, IsQuestCompletedBy = _IsQuestCompletedBy, } function addon:OnInitialize() addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults) DataStore:RegisterModule(addonName, addon, PublicMethods) DataStore:SetCharacterBasedMethod("GetQuestLogSize") DataStore:SetCharacterBasedMethod("GetQuestLogInfo") DataStore:SetCharacterBasedMethod("GetQuestLogNumRewards") DataStore:SetCharacterBasedMethod("GetQuestLogRewardInfo") DataStore:SetCharacterBasedMethod("GetQuestHistory") DataStore:SetCharacterBasedMethod("GetQuestHistoryInfo") DataStore:SetCharacterBasedMethod("IsQuestCompletedBy") end function addon:OnEnable() addon:RegisterEvent("PLAYER_ALIVE", OnPlayerAlive) addon:RegisterEvent("UNIT_QUEST_LOG_CHANGED", OnUnitQuestLogChanged) addon:RegisterEvent("QUEST_COMPLETE", OnQuestComplete) addon:RegisterEvent("QUEST_QUERY_COMPLETE", OnQuestQueryComplete) addon:SetupOptions() if GetOption("AutoUpdateHistory") == 1 then -- if history has been queried at least once, auto update it at logon (fast operation - already in the game's cache) addon:ScheduleTimer(RefreshQuestHistory, 5) -- refresh quest history 5 seconds later, to decrease the load at startup end end function addon:OnDisable() addon:UnregisterEvent("PLAYER_ALIVE") addon:UnregisterEvent("UNIT_QUEST_LOG_CHANGED") addon:UnregisterEvent("QUEST_QUERY_COMPLETE") end -- *** Hooks *** local Orig_QuestRewardCompleteButton_OnClick = QuestRewardCompleteButton_OnClick; function QuestRewardCompleteButton_OnClick() if lastCompleteQuestLink then -- if there's a valid link local questID = lastCompleteQuestLink:match("quest:(%d+):") questID = tonumber(questID) if questID then addon.ThisCharacter.History[questID] = true -- mark the current quest ID as completed addon:SendMessage("DATASTORE_QUEST_TURNED_IN", questID) -- trigger the DS event end lastCompleteQuestLink = nil end Orig_QuestRewardCompleteButton_OnClick(); end QuestFrameCompleteQuestButton:SetScript("OnClick", QuestRewardCompleteButton_OnClick);