mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-20 12:20:50 +00:00
345 lines
10 KiB
Lua
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")
|