Files
Questie-Octo/Quest/Completion.lua
T

345 lines
10 KiB
Lua

QuestieOcto.Completion = QuestieOcto.Completion or {}
local C = QuestieOcto.Completion
C.ready=false
C.receiving=false
C.querying=false
C.queryStartedAt=0
C.history={}
C.current={}
C.sessionLocks={}
C.dailyReset={}
C.receivedPackets=0
C.startedAt=0
C.source="none"
C.nextResetCheck=0
C.stats={refreshes=0,localRefreshes=0,serverRefreshes=0,turnIns=0}
local function SplitWords(text)
local result={}
if not text then return result end
string.gsub(text,"([^%s]+)",function(v) table.insert(result,v) end)
return result
end
local function CopyTrueSet(src,dst)
if type(src)~="table" then return end
for questID,value in pairs(src) do
if value then dst[tonumber(questID) or questID]=true end
end
end
local function LoadSaved()
QuestieOctoDB=QuestieOctoDB or {}
C.history={}
C.dailyReset={}
CopyTrueSet(QuestieOctoDB.completed,C.history)
if type(QuestieOctoDB.dailyReset)=="table" then
for questID,epoch in pairs(QuestieOctoDB.dailyReset) do
questID=tonumber(questID) or questID
epoch=tonumber(epoch)
if epoch then C.dailyReset[questID]=epoch end
end
end
end
local function Save()
QuestieOctoDB=QuestieOctoDB or {}
QuestieOctoDB.completed=C.history
QuestieOctoDB.dailyReset=C.dailyReset
end
local function Publish(source)
C.ready=true
C.source=source or C.source
C.stats.refreshes=(C.stats.refreshes or 0)+1
Save()
QuestieOcto:SendMessage("COMPLETION_READY")
end
local function NextRealmMidnight()
if type(time)~="function" or type(GetGameTime)~="function" then return nil end
local hour,minute=GetGameTime()
hour=tonumber(hour)
minute=tonumber(minute)
if not hour or not minute then return nil end
local remaining=1440-(hour*60+minute)
if remaining<=0 then remaining=1440 end
return time()+(remaining*60)
end
function C:IsDailyLocked(questID)
local epoch=tonumber(self.dailyReset[questID])
if not epoch then return false end
if type(time)=="function" and epoch<=time() then
self.dailyReset[questID]=nil
return false
end
return true
end
function C:IsEverComplete(questID)
return self.history[questID] and true or false
end
function C:IsCurrentComplete(questID)
if self.current[questID] or self.sessionLocks[questID] then return true end
local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil
if q and q.daily and self:IsDailyLocked(questID) then return true end
return false
end
-- Backward-compatible name used by UI/diagnostics. "Complete" here means the
-- character has rewarded the quest at least once, not necessarily that a
-- repeatable/daily quest is currently locked.
function C:IsComplete(questID)
return self:IsEverComplete(questID)
end
function C:IsQuestBlockedByCompletion(questID,q)
q=q or (QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID))
if q and q.daily then return self:IsCurrentComplete(questID) end
if q and q.yearly then return self:IsCurrentComplete(questID) end
if q and q.repeatable then
if q.hideAfterFirstCompletion then
return self:IsEverComplete(questID) or self:IsCurrentComplete(questID)
end
return false
end
return self:IsEverComplete(questID) or self:IsCurrentComplete(questID)
end
function C:IsRewardedForPrerequisite(questID)
local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil
if q and (q.daily or q.yearly) then
return self:IsCurrentComplete(questID)
end
return self:IsEverComplete(questID) or self:IsCurrentComplete(questID)
end
function C:HasBlockingStatus(questID)
local state=QuestieOcto.QuestLog and QuestieOcto.QuestLog.active and QuestieOcto.QuestLog.active[questID]
if state and not state.failed then return true end
local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil
if q and q.repeatable and not q.daily and not q.yearly then
return false
end
return self:IsRewardedForPrerequisite(questID)
end
function C:Count()
local n=0
for _ in pairs(self.history) do n=n+1 end
return n
end
function C:OnQuestTurnedIn(questID)
questID=tonumber(questID)
if not questID then return end
self.stats.turnIns=(self.stats.turnIns or 0)+1
local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil
-- Always remember that the quest was rewarded at least once. Ordinary
-- repeatables are still allowed to reappear by IsQuestBlockedByCompletion;
-- this persistent history is needed when another quest uses them as a
-- historical prerequisite.
self.history[questID]=true
if q and q.daily then
self.current[questID]=true
local reset=NextRealmMidnight()
if reset then self.dailyReset[questID]=reset end
elseif q and q.yearly then
-- No trustworthy client-side yearly calendar is available. Keep a
-- same-session lock and let the authoritative server completion query
-- establish state after a relog.
self.current[questID]=true
self.sessionLocks[questID]=true
elseif q and q.repeatable then
if q.hideAfterFirstCompletion then
self.current[questID]=true
else
self.current[questID]=nil
end
else
self.current[questID]=true
end
Save()
if QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.RemoveQuest then
QuestieOcto.AvailableQuests:RemoveQuest(questID)
end
self.ready=true
self.source="QUEST_TURNED_IN"
QuestieOcto:SendMessage("COMPLETION_READY")
QuestieOcto.Scheduler:After(0.01,function()
if QuestieOcto.AvailableQuests then QuestieOcto.AvailableQuests:FastRefresh() end
end,"questie-turnin-fast-refresh")
end
function C:RefreshFromQuestieAPI()
if not QuestieOcto.API then return false end
local completed=QuestieOcto.API:GetQuestsCompleted()
if type(completed)~="table" then return false end
local nextCurrent={}
for questID,value in pairs(completed) do
if value then
questID=tonumber(questID) or questID
nextCurrent[questID]=true
self.history[questID]=true
end
end
-- Session-only yearly locks survive an in-session API refresh. They are
-- intentionally not SavedVariables because the reset date is server policy.
for questID in pairs(self.sessionLocks) do nextCurrent[questID]=true end
self.current=nextCurrent
self.receiving=false
self.stats.localRefreshes=(self.stats.localRefreshes or 0)+1
Publish("GetQuestsCompleted")
return true
end
function C:StartCompletionQuery()
if self.querying then return true end
if not QuestieOcto.API or not QuestieOcto.API.QueryQuestsCompleted then return false end
if not QuestieOcto.API.optional or not QuestieOcto.API.optional.queryQuestsCompleted then return false end
self.ready=false
self.querying=true
self.queryStartedAt=GetTime()
self.source="QueryQuestsCompleted"
self.frame:RegisterEvent("QUEST_QUERY_COMPLETE")
if not QuestieOcto.API:QueryQuestsCompleted() then
self.querying=false
self.frame:UnregisterEvent("QUEST_QUERY_COMPLETE")
return false
end
return true
end
function C:FinishCompletionQuery()
if not self.querying then return false end
self.querying=false
self.frame:UnregisterEvent("QUEST_QUERY_COMPLETE")
return self:RefreshFromQuestieAPI()
end
function C:StartServerFallback()
if self.receiving then return end
self.ready=false
self.receiving=true
self.current={}
self.receivedPackets=0
self.startedAt=GetTime()
self.source="TWQUEST"
self.frame:RegisterEvent("CHAT_MSG_ADDON")
SendChatMessage(".queststatus","GUILD")
end
function C:Start()
LoadSaved()
-- pfQuest's /db query protocol is important on Vanilla/Turtle: first ask the
-- server to populate the completion cache, then wait for QUEST_QUERY_COMPLETE
-- before reading GetQuestsCompleted(). Reading it immediately can be stale.
if self:StartCompletionQuery() then return end
if self:RefreshFromQuestieAPI() then return end
self:StartServerFallback()
end
function C:FinishServerFallback()
if not self.receiving then return end
self.receiving=false
self.frame:UnregisterEvent("CHAT_MSG_ADDON")
self.stats.serverRefreshes=(self.stats.serverRefreshes or 0)+1
Publish("TWQUEST")
end
function C:RefreshAfterQuestStateChange()
if QuestieOcto.API and QuestieOcto.API.optional and QuestieOcto.API.optional.questsCompleted then
QuestieOcto.Scheduler:After(0.20,function()
if not C:StartCompletionQuery() then C:RefreshFromQuestieAPI() end
end,"completion-local-refresh")
end
end
function C:ScheduleStart()
QuestieOcto.Scheduler:After(0.01,function() C:Start() end,"completion-start")
end
local f=CreateFrame("Frame","QuestieOctoCompletionEvents",UIParent)
C.frame=f
f:RegisterEvent("QUEST_TURNED_IN")
f:SetScript("OnEvent",function()
if event=="QUEST_QUERY_COMPLETE" then
if not C:FinishCompletionQuery() then C:StartServerFallback() end
return
end
if event=="QUEST_TURNED_IN" then
C:OnQuestTurnedIn(arg1)
return
end
if event=="CHAT_MSG_ADDON" and arg1=="TWQUEST" then
C.receivedPackets=C.receivedPackets+1
for _,word in pairs(SplitWords(arg2)) do
local id=tonumber(word)
if id then
C.current[id]=true
C.history[id]=true
end
end
end
end)
f:SetScript("OnUpdate",function()
if C.querying and GetTime()>(C.queryStartedAt or 0)+3.0 then
C.querying=false
C.frame:UnregisterEvent("QUEST_QUERY_COMPLETE")
C:StartServerFallback()
end
if C.receiving and GetTime()>C.startedAt+3.0 then
C:FinishServerFallback()
end
-- Re-evaluate saved daily locks shortly after realm midnight even if the
-- player is standing idle and no quest event fires at the reset boundary.
if GetTime()>(C.nextResetCheck or 0) then
C.nextResetCheck=GetTime()+30
local expired=false
if type(time)=="function" then
local now=time()
for questID,epoch in pairs(C.dailyReset) do
if tonumber(epoch) and tonumber(epoch)<=now then
C.dailyReset[questID]=nil
C.current[questID]=nil
expired=true
end
end
end
if expired then
Save()
if QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.Schedule then
QuestieOcto.AvailableQuests:Schedule(true,0.01)
end
end
end
end)
QuestieOcto:RegisterMessage("QUEST_ELIGIBILITY_STATE_CHANGED",C,"RefreshAfterQuestStateChange")