f4f3de929b
release / release (push) Successful in 5s
Quests, Achievements, Reputations, Pets, Stats, Skills, Crafts, Spells, Talents all had the ghost-gated PLAYER_ALIVE scan (DEBUG 2025-07-21 leftover): they only scanned when the player died and released spirit, so their data never populated on a normal login. Now scan once per session at login (addon.coaScannedThisSession guard), matching the earlier DataStore_Characters/_Inventory fix. This is why reputations/recipes/quests/pets/etc were 'not saved'.
368 lines
11 KiB
Lua
368 lines
11 KiB
Lua
--[[ *** 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);
|