Mercurial > wow > mailopener
view Libs/AceTimer-3.0/AceTimer-3.0.lua @ 1:6f17035de058
MO will now request a server refresh when mail opening has finished, ignoring the timer.
(Collected Module) Fixed the time spent to only be displayed if it is being tracked.
The inventory full sound will properly be played again when your bags change.
| author | Zerotorescue |
|---|---|
| date | Sun, 05 Sep 2010 17:02:19 +0200 |
| parents | 823e33465b6e |
| children |
line wrap: on
line source
--- **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, rescheduled -- 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.1s. This constant may change -- in the future, but for now it seemed like a good compromise in efficiency and accuracy. -- -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you -- need to cancel or reschedule 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 895 2009-12-06 16:28:55Z nevcairiel $ --[[ Basic assumptions: * In a typical system, we do more re-scheduling per second than there are timer pulses per second * Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10) This implementation: CON: The smallest timer interval is constrained by HZ (currently 1/10s). PRO: It will still correctly fire any timer slower than HZ over a length of time, e.g. 0.11s interval -> 90 times over 10 seconds PRO: In lag bursts, the system simly skips missed timer intervals to decrease load CON: Algorithms depending on a timer firing "N times per minute" will fail PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket. CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease. Major assumptions upheld: - ALLOWS scheduling multiple timers with the same funcref/method - ALLOWS scheduling more timers during OnUpdate processing - ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing ]] local MAJOR, MINOR = "AceTimer-3.0", 5 local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not AceTimer then return end -- No upgrade needed AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member) -- Linked list gets around ACE-88 and ACE-90. AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...} AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame") -- Lua APIs local assert, error, loadstring = assert, error, loadstring local setmetatable, rawset, rawget = setmetatable, rawset, rawget local select, pairs, type, next, tostring = select, pairs, type, next, tostring local floor, max, min = math.floor, math.max, math.min local tconcat = table.concat -- WoW APIs local GetTime = GetTime -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded -- List them here for Mikk's FindGlobals script -- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler -- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes. local timerCache = nil --[[ Timers will not be fired more often than HZ-1 times per second. Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999) If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade. If this number is ever changed, all entries need to be rehashed on lib upgrade. ]] local HZ = 11 --[[ Prime for good distribution If this number is ever changed, all entries need to be rehashed on lib upgrade. ]] local BUCKETS = 131 local hash = AceTimer.hash for i=1,BUCKETS do hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes end --[[ xpcall safecall implementation ]] local xpcall = xpcall local function errorhandler(err) return geterrorhandler()(err) end local function CreateDispatcher(argCount) local code = [[ local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration local method, ARGS local function call() return method(ARGS) end local function dispatch(func, ...) method = func if not method then return end ARGS = ... return xpcall(call, eh) end return dispatch ]] local ARGS = {} for i = 1, argCount do ARGS[i] = "arg"..i end code = code:gsub("ARGS", tconcat(ARGS, ", ")) return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler) end local Dispatchers = setmetatable({}, { __index=function(self, argCount) local dispatcher = CreateDispatcher(argCount) rawset(self, argCount, dispatcher) return dispatcher end }) Dispatchers[0] = function(func) return xpcall(func, errorhandler) end local function safecall(func, ...) return Dispatchers[select('#', ...)](func, ...) end local lastint = floor(GetTime() * HZ) -- -------------------------------------------------------------------- -- OnUpdate handler -- -- traverse buckets, always chasing "now", and fire timers that have expired local function OnUpdate() local now = GetTime() local nowint = floor(now * HZ) -- Have we passed into a new hash bucket? if nowint == lastint then return end local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2 -- Pass through each bucket at most once -- Happens on e.g. instance loads, but COULD happen on high local load situations also for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration local curbucket = (curint % BUCKETS)+1 -- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks. local nexttimer = hash[curbucket] hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash while nexttimer do local timer = nexttimer nexttimer = timer.next local when = timer.when if when < soon then -- Call the timer func, either as a method on given object, or a straight function ref local callback = timer.callback if type(callback) == "string" then safecall(timer.object[callback], timer.object, timer.arg) elseif callback then safecall(callback, timer.arg) else -- probably nilled out by CancelTimer timer.delay = nil -- don't reschedule it end local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback if not delay then -- single-shot timer (or cancelled) AceTimer.selfs[timer.object][tostring(timer)] = nil timerCache = timer else -- repeating timer local newtime = when + delay if newtime < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.) newtime = now + delay end timer.when = newtime -- add next timer execution to the correct bucket local bucket = (floor(newtime * HZ) % BUCKETS) + 1 timer.next = hash[bucket] hash[bucket] = timer end else -- if when>=soon -- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution) timer.next = hash[curbucket] hash[curbucket] = timer end -- if when<soon ... else end -- while nexttimer do end -- for curint=lastint,nowint lastint = nowint end -- --------------------------------------------------------------------- -- Reg( callback, delay, arg, repeating ) -- -- callback( function or string ) - direct function ref or method name in our object for the callback -- delay(int) - delay for the timer -- arg(variant) - any argument to be passed to the callback function -- repeating(boolean) - repeating timer, or oneshot -- -- returns the handle of the timer for later processing (canceling etc) local function Reg(self, callback, delay, arg, repeating) if type(callback) ~= "string" and type(callback) ~= "function" then local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" error(MAJOR..": " .. error_origin .. "(callback, delay, arg): 'callback' - function or method name expected.", 3) end if type(callback) == "string" then if type(self)~="table" then local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'self' - must be a table.", 3) end if type(self[callback]) ~= "function" then local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'methodName' - method not found on target object.", 3) end end if delay < (1 / (HZ - 1)) then delay = 1 / (HZ - 1) end -- Create and stuff timer in the correct hash bucket local now = GetTime() local timer = timerCache or {} -- Get new timer object (from cache if available) timerCache = nil timer.object = self timer.callback = callback timer.delay = (repeating and delay) timer.arg = arg timer.when = now + delay local bucket = (floor((now+delay)*HZ) % BUCKETS) + 1 timer.next = hash[bucket] hash[bucket] = timer -- Insert timer in our self->handle->timer registry local handle = tostring(timer) local selftimers = AceTimer.selfs[self] if not selftimers then selftimers = {} AceTimer.selfs[self] = selftimers end selftimers[handle] = timer selftimers.__ops = (selftimers.__ops or 0) + 1 return handle 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 arg An optional argument to be passed to the callback function. -- @usage -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0") -- -- function MyAddon:OnEnable() -- self:ScheduleTimer("TimerFeedback", 5) -- end -- -- function MyAddon:TimerFeedback() -- print("5 seconds passed") -- end function AceTimer:ScheduleTimer(callback, delay, arg) return Reg(self, callback, delay, arg) 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 arg An optional argument to be passed to the callback function. -- @usage -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "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(callback, delay, arg) return Reg(self, callback, delay, arg, true) end --- Cancels a timer with the given handle, 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 `handle` is valid -- and the timer has not fired yet or was canceled before. -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` -- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled) -- @return True if the timer was successfully cancelled. function AceTimer:CancelTimer(handle, silent) if not handle then return end -- nil handle -> bail out without erroring if type(handle) ~= "string" then error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway end local selftimers = AceTimer.selfs[self] local timer = selftimers and selftimers[handle] if silent then if timer then timer.callback = nil -- don't run it again timer.delay = nil -- if this is the currently-executing one: don't even reschedule -- The timer object is removed in the OnUpdate loop end return not not timer -- might return "true" even if we double-cancel. we'll live. else if not timer then geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered") return false end if not timer.callback then geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired") return false end timer.callback = nil -- don't run it again timer.delay = nil -- if this is the currently-executing one: don't even reschedule return true end end --- Cancels all timers registered to the current addon object ('self') function AceTimer:CancelAllTimers() if not(type(self) == "string" or type(self) == "table") then error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2) end if self == AceTimer then error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2) end local selftimers = AceTimer.selfs[self] if selftimers then for handle,v in pairs(selftimers) do if type(v) == "table" then -- avoid __ops, etc AceTimer.CancelTimer(self, handle, true) end end end end --- Returns the time left for a timer with the given handle, registered by the current addon object ('self'). -- This function will raise a warning when the handle is invalid, but not stop execution. -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` -- @return The time left on the timer, or false if the handle is invalid. function AceTimer:TimeLeft(handle) if not handle then return end if type(handle) ~= "string" then error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway end local selftimers = AceTimer.selfs[self] local timer = selftimers and selftimers[handle] if not timer then geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered") return false end return timer.when - GetTime() end -- --------------------------------------------------------------------- -- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step -- and clean it out - otherwise the table indices can grow indefinitely -- if an addon starts and stops a lot of timers. AceBucket does this! -- -- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua local lastCleaned = nil local function OnEvent(this, event) if event~="PLAYER_REGEN_ENABLED" then return end -- Get the next 'self' to process local selfs = AceTimer.selfs local self = next(selfs, lastCleaned) if not self then self = next(selfs) end lastCleaned = self if not self then -- should only happen if .selfs[] is empty return end -- Time to clean it out? local list = selfs[self] if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'. return end -- Create a new table and copy all members over local newlist = {} local n=0 for k,v in pairs(list) do newlist[k] = v n=n+1 end newlist.__ops = 0 -- Reset operation count -- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not. if n>BUCKETS then DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?") end selfs[self] = newlist 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 -- --------------------------------------------------------------------- -- Debug tools (expose copies of internals to test suites) AceTimer.debug = AceTimer.debug or {} AceTimer.debug.HZ = HZ AceTimer.debug.BUCKETS = BUCKETS -- --------------------------------------------------------------------- -- Finishing touchups AceTimer.frame:SetScript("OnUpdate", OnUpdate) AceTimer.frame:SetScript("OnEvent", OnEvent) AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- In theory, we should hide&show the frame based on there being timers or not. -- However, this job is fairly expensive, and the chance that there will -- actually be zero timers running is diminuitive to say the lest.
