tercio@0: --- **AceTimer-3.0** provides a central facility for registering timers. tercio@0: -- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient tercio@0: -- data structure that allows easy dispatching and fast rescheduling. Timers can be registered tercio@0: -- or canceled at any time, even from within a running timer, without conflict or large overhead.\\ tercio@5: -- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API tercio@5: -- restricts us to. tercio@0: -- tercio@0: -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you tercio@0: -- need to cancel the timer you just registered. tercio@0: -- tercio@0: -- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by tercio@0: -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object tercio@0: -- and can be accessed directly, without having to explicitly call AceTimer itself.\\ tercio@0: -- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you tercio@0: -- make into AceTimer. tercio@0: -- @class file tercio@0: -- @name AceTimer-3.0 Tercio@20: -- @release $Id: AceTimer-3.0.lua 1170 2018-03-29 17:38:58Z funkydude $ tercio@0: tercio@5: local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes tercio@0: local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) tercio@0: tercio@0: if not AceTimer then return end -- No upgrade needed tercio@5: AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list tercio@5: local activeTimers = AceTimer.activeTimers -- Upvalue our private data tercio@0: tercio@0: -- Lua APIs tercio@5: local type, unpack, next, error, select = type, unpack, next, error, select tercio@5: -- WoW APIs tercio@5: local GetTime, C_TimerAfter = GetTime, C_Timer.After tercio@0: tercio@5: local function new(self, loop, func, delay, ...) tercio@5: if delay < 0.01 then tercio@5: delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us tercio@0: end tercio@0: Tercio@20: local timer = { Tercio@20: object = self, Tercio@20: func = func, Tercio@20: looping = loop, Tercio@20: argsCount = select("#", ...), Tercio@20: delay = delay, Tercio@20: ends = GetTime() + delay, Tercio@20: ... Tercio@20: } tercio@0: tercio@5: activeTimers[timer] = timer tercio@5: tercio@5: -- Create new timer closure to wrap the "timer" object tercio@5: timer.callback = function() tercio@5: if not timer.cancelled then tercio@5: if type(timer.func) == "string" then tercio@5: -- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil tercio@5: -- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue. tercio@5: timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount)) tercio@5: else tercio@5: timer.func(unpack(timer, 1, timer.argsCount)) tercio@5: end tercio@5: tercio@5: if timer.looping and not timer.cancelled then tercio@5: -- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly tercio@5: -- due to fps differences tercio@5: local time = GetTime() tercio@5: local delay = timer.delay - (time - timer.ends) tercio@5: -- Ensure the delay doesn't go below the threshold tercio@5: if delay < 0.01 then delay = 0.01 end tercio@5: C_TimerAfter(delay, timer.callback) tercio@5: timer.ends = time + delay tercio@5: else tercio@5: activeTimers[timer.handle or timer] = nil tercio@5: end tercio@5: end tercio@0: end tercio@0: tercio@5: C_TimerAfter(delay, timer.callback) tercio@5: return timer tercio@0: end tercio@0: tercio@0: --- Schedule a new one-shot timer. tercio@0: -- The timer will fire once in `delay` seconds, unless canceled before. tercio@0: -- @param callback Callback function for the timer pulse (funcref or method name). tercio@0: -- @param delay Delay for the timer, in seconds. tercio@0: -- @param ... An optional, unlimited amount of arguments to pass to the callback function. tercio@0: -- @usage tercio@0: -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0") tercio@0: -- tercio@0: -- function MyAddOn:OnEnable() tercio@0: -- self:ScheduleTimer("TimerFeedback", 5) tercio@0: -- end tercio@0: -- tercio@0: -- function MyAddOn:TimerFeedback() tercio@0: -- print("5 seconds passed") tercio@0: -- end tercio@0: function AceTimer:ScheduleTimer(func, delay, ...) tercio@0: if not func or not delay then tercio@0: error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2) tercio@0: end tercio@0: if type(func) == "string" then tercio@0: if type(self) ~= "table" then tercio@0: error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2) tercio@0: elseif not self[func] then tercio@0: error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2) tercio@0: end tercio@0: end tercio@0: return new(self, nil, func, delay, ...) tercio@0: end tercio@0: tercio@0: --- Schedule a repeating timer. tercio@0: -- The timer will fire every `delay` seconds, until canceled. tercio@0: -- @param callback Callback function for the timer pulse (funcref or method name). tercio@0: -- @param delay Delay for the timer, in seconds. tercio@0: -- @param ... An optional, unlimited amount of arguments to pass to the callback function. tercio@0: -- @usage tercio@0: -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0") tercio@0: -- tercio@0: -- function MyAddOn:OnEnable() tercio@0: -- self.timerCount = 0 tercio@0: -- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5) tercio@0: -- end tercio@0: -- tercio@0: -- function MyAddOn:TimerFeedback() tercio@0: -- self.timerCount = self.timerCount + 1 tercio@0: -- print(("%d seconds passed"):format(5 * self.timerCount)) tercio@0: -- -- run 30 seconds in total tercio@0: -- if self.timerCount == 6 then tercio@0: -- self:CancelTimer(self.testTimer) tercio@0: -- end tercio@0: -- end tercio@0: function AceTimer:ScheduleRepeatingTimer(func, delay, ...) tercio@0: if not func or not delay then tercio@0: error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2) tercio@0: end tercio@0: if type(func) == "string" then tercio@0: if type(self) ~= "table" then tercio@0: error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2) tercio@0: elseif not self[func] then tercio@0: error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2) tercio@0: end tercio@0: end tercio@0: return new(self, true, func, delay, ...) tercio@0: end tercio@0: tercio@0: --- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer` tercio@0: -- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid tercio@0: -- and the timer has not fired yet or was canceled before. tercio@0: -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` tercio@0: function AceTimer:CancelTimer(id) tercio@0: local timer = activeTimers[id] tercio@0: tercio@5: if not timer then tercio@5: return false tercio@5: else tercio@5: timer.cancelled = true tercio@5: activeTimers[id] = nil tercio@5: return true tercio@5: end tercio@0: end tercio@0: tercio@0: --- Cancels all timers registered to the current addon object ('self') tercio@0: function AceTimer:CancelAllTimers() Tercio@20: for k,v in next, activeTimers do tercio@0: if v.object == self then tercio@0: AceTimer.CancelTimer(self, k) tercio@0: end tercio@0: end tercio@0: end tercio@0: tercio@0: --- Returns the time left for a timer with the given id, registered by the current addon object ('self'). tercio@0: -- This function will return 0 when the id is invalid. tercio@0: -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` tercio@0: -- @return The time left on the timer. tercio@0: function AceTimer:TimeLeft(id) tercio@0: local timer = activeTimers[id] tercio@5: if not timer then tercio@5: return 0 tercio@5: else tercio@5: return timer.ends - GetTime() tercio@5: end tercio@0: end tercio@0: tercio@0: tercio@0: -- --------------------------------------------------------------------- tercio@0: -- Upgrading tercio@0: tercio@5: -- Upgrade from old hash-bucket based timers to C_Timer.After timers. tercio@0: if oldminor and oldminor < 10 then tercio@0: -- disable old timer logic tercio@0: AceTimer.frame:SetScript("OnUpdate", nil) tercio@0: AceTimer.frame:SetScript("OnEvent", nil) tercio@0: AceTimer.frame:UnregisterAllEvents() tercio@0: -- convert timers Tercio@20: for object,timers in next, AceTimer.selfs do Tercio@20: for handle,timer in next, timers do tercio@0: if type(timer) == "table" and timer.callback then tercio@5: local newTimer tercio@0: if timer.delay then tercio@5: newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg) tercio@0: else tercio@5: newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg) tercio@0: end tercio@5: -- Use the old handle for old timers tercio@5: activeTimers[newTimer] = nil tercio@5: activeTimers[handle] = newTimer tercio@5: newTimer.handle = handle tercio@0: end tercio@0: end tercio@0: end tercio@0: AceTimer.selfs = nil tercio@0: AceTimer.hash = nil tercio@0: AceTimer.debug = nil tercio@5: elseif oldminor and oldminor < 17 then tercio@5: -- Upgrade from old animation based timers to C_Timer.After timers. tercio@5: AceTimer.inactiveTimers = nil tercio@5: AceTimer.frame = nil tercio@5: local oldTimers = AceTimer.activeTimers tercio@5: -- Clear old timer table and update upvalue tercio@5: AceTimer.activeTimers = {} tercio@5: activeTimers = AceTimer.activeTimers Tercio@20: for handle, timer in next, oldTimers do tercio@5: local newTimer tercio@5: -- Stop the old timer animation tercio@5: local duration, elapsed = timer:GetDuration(), timer:GetElapsed() tercio@5: timer:GetParent():Stop() tercio@5: if timer.looping then tercio@5: newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount)) tercio@5: else tercio@5: newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount)) tercio@0: end tercio@5: -- Use the old handle for old timers tercio@5: activeTimers[newTimer] = nil tercio@5: activeTimers[handle] = newTimer tercio@5: newTimer.handle = handle tercio@0: end tercio@0: tercio@5: -- Migrate transitional handles tercio@5: if oldminor < 13 and AceTimer.hashCompatTable then Tercio@20: for handle, id in next, AceTimer.hashCompatTable do tercio@5: local t = activeTimers[id] tercio@5: if t then tercio@5: activeTimers[id] = nil tercio@5: activeTimers[handle] = t tercio@5: t.handle = handle tercio@5: end tercio@5: end tercio@5: AceTimer.hashCompatTable = nil tercio@5: end tercio@0: end tercio@0: tercio@0: -- --------------------------------------------------------------------- tercio@0: -- Embed handling tercio@0: tercio@0: AceTimer.embeds = AceTimer.embeds or {} tercio@0: tercio@0: local mixins = { tercio@0: "ScheduleTimer", "ScheduleRepeatingTimer", tercio@0: "CancelTimer", "CancelAllTimers", tercio@0: "TimeLeft" tercio@0: } tercio@0: tercio@0: function AceTimer:Embed(target) tercio@0: AceTimer.embeds[target] = true Tercio@20: for _,v in next, mixins do tercio@0: target[v] = AceTimer[v] tercio@0: end tercio@0: return target tercio@0: end tercio@0: tercio@0: -- AceTimer:OnEmbedDisable(target) tercio@0: -- target (object) - target object that AceTimer is embedded in. tercio@0: -- tercio@0: -- cancel all timers registered for the object tercio@0: function AceTimer:OnEmbedDisable(target) tercio@0: target:CancelAllTimers() tercio@0: end tercio@0: Tercio@20: for addon in next, AceTimer.embeds do tercio@0: AceTimer:Embed(addon) tercio@0: end