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