-- A pool of objects for determining what text to display for a given cooldown -- and notify subscribers when the text change -- local bindings! local AddonName, Addon = ... local L = LibStub('AceLocale-3.0'):GetLocale(AddonName) -- time units in ms local TENTH = 100 local SECOND = TENTH * 10 local MINUTE = SECOND * 60 local HOUR = MINUTE * 60 local DAY = HOUR * 24 -- rounding values in ms local HALF_TENTH = TENTH / 2 local HALF_SECOND = SECOND / 2 local HALF_MINUTE = MINUTE / 2 local HALF_HOUR = HOUR / 2 local HALF_DAY = DAY / 2 -- transition points in ms local SOON_THRESHOLD = SECOND * 5.5 -- 5.5 seconds local SECONDS_THRESHOLD = MINUTE - HALF_SECOND -- 59.5 seconds local MINUTES_THRESHOLD = HOUR - HALF_MINUTE -- 59.5 minutes local HOURS_THRESHOLD = DAY - HALF_HOUR -- 23.5 hours ---@type { [string]: OmniCCTimer } local active = {} ---@type { [OmniCCTimer]: true } local inactive = setmetatable({}, {__mode = 'k'}) ---@class OmniCCTimer local Timer = {} Timer.__index = Timer ---@param cooldown OmniCCCooldown function Timer:GetOrCreate(cooldown) local start = cooldown._occ_start or 0 if start <= 0 then return end local duration = cooldown._occ_duration or 0 if duration <= 0 then return end local settings = cooldown._occ_settings if settings and settings.timerOffset ~= 0 then duration = duration - (settings.timerOffset/SECOND) if duration <= 0 then return end end local endTime = (start + duration) * SECOND local kind = cooldown._occ_kind local key = strjoin('/', kind, tostring(endTime), tostring(settings or 'NONE')) local timer = active[key] if not timer then timer = next(inactive) if timer then inactive[timer] = nil else timer = setmetatable({}, Timer) end timer.endTime = endTime timer.key = key timer.kind = kind timer.settings = settings timer.subscribers = {} timer.callback = function() timer:Update(key) end timer:Update(key) active[key] = timer end return timer end function Timer:Destroy() if not self.key then return end active[self.key] = nil -- clear subscribers for subscriber in pairs(self.subscribers) do subscriber:OnTimerDestroyed(self) end -- reset fields self.endTime = nil self.key = nil self.kind = nil self.settings = nil self.state = nil self.subscribers = nil self.text = nil inactive[self] = true end function Timer:Refresh() local key = self.key if key then self:Update(key) end end ---@param key string? function Timer:Update(key) if not (key and self.key == key) then return end local remain = (self.endTime or 0) - (GetTime() * SECOND) if remain <= 0 then self.finished = true self:Destroy() return end local text, textSleep = self:GetTimerText(remain) if self.text ~= text then self.text = text for subscriber in pairs(self.subscribers) do subscriber:OnTimerTextUpdated(self, text) end end local state, stateSleep = self:GetTimerState(remain) if self.state ~= state then self.state = state for subscriber in pairs(self.subscribers) do subscriber:OnTimerStateUpdated(self, state) end end local sleep = min(textSleep, stateSleep) if sleep < DAY then Addon.Updater:ScheduleTimer(self.callback, max(sleep / SECOND, 0.015)) end end function Timer:Subscribe(subscriber) if not self.key then return end if not self.subscribers[subscriber] then self.subscribers[subscriber] = true end end function Timer:Unsubscribe(subscriber) if not self.key then return end if self.subscribers[subscriber] then self.subscribers[subscriber] = nil if not next(self.subscribers) then self:Destroy() end end end -- Calculates timer text ---@param remain number -- The remaining time on the timer, in miliseconds ---@return string? -- The formatted text for the time remaining ---@return number -- How long, in miliseconds, until the the next text update function Timer:GetTimerText(remain) if remain <= 0 then return '', DAY end local tenthsThreshold, mmSSThreshold local sets = self.settings if sets then tenthsThreshold = (sets.tenthsDuration or 0) * SECOND mmSSThreshold = (sets.mmSSDuration or 0) * SECOND else tenthsThreshold = 0 mmSSThreshold = 0 end if remain < tenthsThreshold then -- tenths of seconds local tenths = (remain + HALF_TENTH) - (remain + HALF_TENTH) % TENTH local sleep = remain - (tenths - HALF_TENTH) if tenths > 0 then return L.TenthsFormat:format(tenths / SECOND), sleep end return '', sleep elseif remain < SECONDS_THRESHOLD then -- seconds local seconds = (remain + HALF_SECOND) - (remain + HALF_SECOND) % SECOND local sleep = remain - max(seconds - HALF_SECOND, tenthsThreshold) if seconds > 0 then return L.SecondsFormat:format(seconds / SECOND), sleep end return '', sleep elseif remain < mmSSThreshold then -- MM:SS local seconds = (remain + HALF_SECOND) - (remain + HALF_SECOND) % SECOND local sleep = remain - max(seconds - HALF_SECOND, SECONDS_THRESHOLD) return L.MMSSFormat:format(seconds / MINUTE, (seconds % MINUTE) / SECOND), sleep elseif remain < MINUTES_THRESHOLD then -- minutes local minutes = (remain + HALF_MINUTE) - (remain + HALF_MINUTE) % MINUTE local wait = max( -- transition point of showing one minute versus another (29.5s, 89.5s, 149.5s, ...) minutes - HALF_MINUTE, -- transition point of displaying minutes to displaying seconds (59.5s) SECONDS_THRESHOLD, -- transition point of displaying MM:SS (user set) mmSSThreshold ) local sleep = remain - wait return L.MinutesFormat:format(minutes / MINUTE), sleep elseif remain < HOURS_THRESHOLD then -- hours local hours = (remain + HALF_HOUR) - (remain + HALF_HOUR) % HOUR local sleep = remain - max(hours - HALF_HOUR, MINUTES_THRESHOLD) return L.HoursFormat:format(hours / HOUR), sleep elseif remain <= (DAY * 7) then -- days local days = (remain + HALF_DAY) - (remain + HALF_DAY) % DAY local sleep = remain - max(days - HALF_DAY, HOURS_THRESHOLD) return L.DaysFormat:format(days / DAY), sleep else return '', DAY end end -- Calculates timer state ---@param remain number -- The remaining time on the timer, in miliseconds ---@return OmniCCTimerState -- The curent state of the timer ---@return number -- How long, in miliseconds, until the next timer state function Timer:GetTimerState(remain) if self.kind == 'loc' then return 'controlled', math.huge elseif self.kind == 'charge' then return 'charging', math.huge elseif remain < SOON_THRESHOLD then return 'soon', math.huge elseif remain < SECONDS_THRESHOLD then return 'seconds', remain - SOON_THRESHOLD elseif remain < MINUTES_THRESHOLD then return 'minutes', remain - SECONDS_THRESHOLD else return 'hours', remain - MINUTES_THRESHOLD end end function Timer:ForActive(method, ...) local func = self[method] if type(func) ~= 'function' then return end for _, timer in pairs(active) do func(timer, ...) end end -- exports Addon.Timer = Timer