277 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --- **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
 | |
| -- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
 | |
| 
 | |
| 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 = {...}
 | |
| 	timer.object = self
 | |
| 	timer.func = func
 | |
| 	timer.looping = loop
 | |
| 	timer.argsCount = select("#", ...)
 | |
| 	timer.delay = delay
 | |
| 	timer.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()
 | |
| 				local delay = timer.delay - (time - timer.ends)
 | |
| 				-- Ensure the delay doesn't go below the threshold
 | |
| 				if delay < 0.01 then delay = 0.01 end
 | |
| 				C_TimerAfter(delay, timer.callback)
 | |
| 				timer.ends = time + delay
 | |
| 			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 pairs(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 pairs(AceTimer.selfs) do
 | |
| 		for handle,timer in pairs(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 pairs(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 pairs(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 pairs(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 pairs(AceTimer.embeds) do
 | |
| 	AceTimer:Embed(addon)
 | |
| end
 |