Compare with Previous | Blame | View Log
--[[ -- -- Chronos -- Keeper of Time -- -- By AnduinLothar, Alexander Brazie, and Thott -- -- Chronos manages time. You can schedule a function to be called -- in X seconds, with or without an id. You can request a timer, -- which tracks the elapsed duration since the timer was started. -- -- To use as an embeddable addon: -- - Put the Chronos folder inside your Interface/AddOns/<YourAddonName>/ folder. -- - Add Chronos\Chronos.xml to your toc or load it in your xml before your localization files. -- - Add Chronos to the OptionalDeps in your toc -- -- To use as an addon library: -- - Put the Chronos folder inside your Interface/AddOns/ folder. -- - Add Chronos to the Dependencies in your toc -- -- Please see below or see http://www.wowwiki.com/Chronos_(addon) for details. -- -- $LastChangedBy: karlkfi $ -- $Date: 2006-12-21 06:19:14 -0600 (Thu, 21 Dec 2006) $ -- $Rev: 4467 $ -- --]] local CHRONOS_REV = 2.12; local isBetterInstanceLoaded = ( Chronos and Chronos.version and Chronos.version >= CHRONOS_REV ); if (not isBetterInstanceLoaded) then if (not Chronos) then Chronos = {}; end Chronos.version = CHRONOS_REV; ------------------------------------------------------------------------------ --[[ Variables ]]-- ------------------------------------------------------------------------------ Chronos.online = true; CHRONOS_DEBUG = false; CHRONOS_DEBUG_WARNINGS = false; -- Chronos Data if (not ChronosData) then ChronosData = {}; end -- Chronos Recycled Tables Storage if (not Chronos.tables) then Chronos.tables = {}; end -- Initialize the Timers if (not ChronosData.timers) then ChronosData.timers = {}; end -- Initialize the perform-over-time task list if (not ChronosData.tasks) then ChronosData.tasks = {}; end -- Maximum items per frame Chronos.MAX_TASKS_PER_FRAME = 100; -- Maximum steps per task Chronos.MAX_STEPS_PER_TASK = 300; -- Maximum time delay per frame Chronos.MAX_TIME_PER_STEP = .3; Chronos.emptyTable = {}; ------------------------------------------------------------------------------ --[[ User Functions ]]-- ------------------------------------------------------------------------------ --[[ -- debug(boolean) -- -- Toggles debug mode ]]-- function Chronos.debug(enable) if (enable) then ChronosFrame:SetScript("OnUpdate", Chronos.OnUpdate_Debug); CHRONOS_DEBUG = true; CHRONOS_DEBUG_WARNINGS = true; else ChronosFrame:SetScript("OnUpdate", Chronos.OnUpdate_Quick); CHRONOS_DEBUG = false; CHRONOS_DEBUG_WARNINGS = false; end end --[[ -- Scheduling functions -- Parts rewritten by AnduinLothar for efficiency -- Parts rewritten by Thott for speed -- Written by Alexander -- Original by Thott -- -- Usage: Chronos.schedule(when,handler,arg1,arg2,etc) -- -- After <when> seconds pass (values less than one and fractional values are -- fine), handler is called with the specified arguments, i.e.: -- handler(arg1,arg2,etc) -- -- If you'd like to have something done every X seconds, reschedule -- it each time in the handler or preferably use scheduleRepeating. -- -- Also, please note that there is a limit to the number of -- scheduled tasks that can be performed per xml object at the -- same time. --]] function Chronos.schedule(when, handler, ...) if ( not Chronos.online ) then return; end if ( not handler) then Chronos.printError("ERROR: nil handler passed to Chronos.schedule()"); return; end --local memstart = collectgarbage("count"); -- -- Assign an id -- local id = ""; -- if ( not this ) then -- id = "Keybinding"; -- else -- id = this:GetName(); -- end -- if ( not id ) then -- id = "_DEFAULT"; -- end -- if ( not when ) then -- Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS", "Chronos Error Detection: ", id , " has sent no interval for this function. ", when ); -- return; -- end -- -- Ensure we're not looping ChronosFrame -- if ( id == "ChronosFrame" and ChronosData.lastID ) then -- id = ChronosData.lastID; -- end -- use recycled tables to avoid excessive garbage collection -AnduinLothar --tinsert(ChronosData.sched, Chronos.getTable()) --local i = #ChronosData.sched local recTable = Chronos.getTable() -- ChronosData.sched[i].id = id; recTable.time = when + GetTime(); recTable.handler = handler; recTable.args = Chronos.getArgTable(...); -- task list is a heap, add new local i = #ChronosData.sched+1 while(i > 1) do if(recTable.time < ChronosData.sched[i-1].time) then i = i - 1; else break end end tinsert(ChronosData.sched, i, recTable) -- Debug print --Chronos.printDebugError("CHRONOS_DEBUG", "Scheduled "..handler.." in "..when.." seconds from "..id ); --Chronos.printError("Memory change in schedule: "..memstart.."->"..memend.." = "..memend-memstart); end --[[ -- Chronos.scheduleByName(name, delay, function, arg1, ... ); -- -- Same as Chronos.schedule, except it takes a schedule name argument. -- Only one event can be scheduled with a given name at any one time. -- Thus if one exists, and another one is scheduled, the first one -- is deleted, then the second one added. -- --]] function Chronos.scheduleByName(name, when, handler, ...) if ( not name ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No name specified to Chronos.scheduleByName"); return; end local namedSchedule = ChronosData.byName[name]; if(namedSchedule and handler) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: scheduleByName is reasigning \"".. name.."\"."); Chronos.releaseTable(ChronosData.byName[name]); else if ( not handler ) then if ( not namedSchedule ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No handler specified to Chronos.scheduleByName, no previous entry found for scheduled entry \"".. name.."\"."); return; end if ( not namedSchedule.handler ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No handler specified to Chronos.scheduleByName, no handler could be found in previous entry of \"".. name.."\" either."); return; end handler = namedSchedule.handler; Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: scheduleByName is updating \"".. name.."\" to time: ".. when); else Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: scheduleByName is asigning \"".. name.."\"."); end end ChronosData.byName[name] = Chronos.getTable(); namedSchedule = ChronosData.byName[name]; namedSchedule.time = when+GetTime() namedSchedule.handler = handler; namedSchedule.args = Chronos.getArgTable(...); end --[[ -- unscheduleByName(name); -- -- Removes an entry that was created with scheduleByName() -- -- Args: -- name - the name used -- --]] function Chronos.unscheduleByName(name) if ( not Chronos.online ) then return; end if ( not name ) then Chronos.printError("No name specified to Chronos.unscheduleByName"); return; end if(ChronosData.byName[name]) then Chronos.releaseTable(ChronosData.byName[name]); ChronosData.byName[name] = nil; end -- Debug print --Chronos.printDebugError("CHRONOS_DEBUG", "Cancelled scheduled timer of name ",name); end --[[ -- unscheduleRepeating(name); -- Mirrors unscheduleByName for backwards compatibility --]] Chronos.unscheduleRepeating = Chronos.unscheduleByName; --[[ -- isScheduledByName(name) -- Returns the amount of time left if it is indeed scheduled by name! -- -- returns: -- number - time remaining -- nil - not scheduled -- --]] function Chronos.isScheduledByName(name) if ( not Chronos.online ) then return; end if ( not name ) then Chronos.printError("No name specified to Chronos.isScheduledByName "..(this:GetName() or "unknown")); return; end local namedSchedule = ChronosData.byName[name]; if(namedSchedule) then return namedSchedule.time - GetTime(); end -- Debug print --Chronos.printDebugError("CHRONOS_DEBUG", "Did not find timer of name ",name); return nil; end --[[ -- isScheduledRepeating(name) -- Mirrors isScheduledByName for backwards compatibility --]] Chronos.isScheduledRepeating = Chronos.isScheduledByName; --[[ -- Chronos.scheduleRepeating(name, delay, function); -- -- Same as Chronos.scheduleByName, except it repeats without recalling and takes no arguments. -- --]] function Chronos.scheduleRepeating(name, when, handler) if ( not name ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No name specified to Chronos.scheduleRepeating"); return; end local namedSchedule = ChronosData.byName[name]; if(namedSchedule and handler) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: scheduleRepeating is reasigning ".. name); Chronos.releaseTable(ChronosData.byName[name]); else if ( not handler ) then if ( not namedSchedule ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No handler specified to Chronos.scheduleRepeating, no previous entry found for scheduled entry '".. name.."'."); return; end if ( not namedSchedule.handler ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No handler specified to Chronos.scheduleRepeating, no handler could be found in previous entry '".. name.."' either."); return; end handler = namedSchedule.handler; Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: scheduleRepeating is updating '".. name.."' to time: ".. when); else Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: scheduleRepeating is asigning '".. name.."'."); end end ChronosData.byName[name] = Chronos.getTable(); namedSchedule = ChronosData.byName[name]; namedSchedule.time = when+GetTime(); namedSchedule.period = when; namedSchedule.handler = handler; namedSchedule.repeating = true; end --[[ -- Chronos.flushByName(name, when); -- -- Updates the ByName or Repeating event to flush at the time specified. If no time is specified flush will be immediate. If it is a Repeating event the timer will be reset. -- --]] function Chronos.flushByName(name, when) if ( not name ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: No name specified to Chronos.flushByName"); return; elseif ( not ChronosData.byName[name] ) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos Error Detection: no previous entry found for Chronos.flushByName entry '".. name.."'."); return; end if (not when) then Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: flushing '".. name.."'."); when = GetTime(); else Chronos.printDebugError("CHRONOS_DEBUG_WARNINGS","Chronos: flushing '".. name.."' in "..when.." seconds."); when = when+GetTime(); end ChronosData.byName[name].time = when; end --[[ -- Chronos.startTimer([ID]); -- Starts a timer on a particular -- -- Args -- ID - optional parameter to identify who is asking for a timer. -- -- If ID does not exist, this:GetName() is used. -- -- When you want to get the amount of time passed since startTimer(ID) is called, -- call getTimer(ID) and it will return the number in seconds. -- --]] function Chronos.startTimer( id ) if ( not Chronos.online ) then return; end if ( not id ) then id = this:GetName(); end -- Create a table for this id's timers if ( not ChronosData.timers[id] ) then ChronosData.timers[id] = Chronos.getTable(); end -- Clear out an entry if the table is too big. if (#ChronosData.timers[id] > Chronos.MAX_TASKS_PER_FRAME) then Chronos.printError("Too many Chronos timers created for id " .. tostring(id)); return; end -- Add a new timer entry table.insert(ChronosData.timers[id], GetTime()); end --[[ -- endTimer([id]); -- -- Ends the timer and returns the amount of time passed. -- -- args: -- id - ID for the timer. If not specified, then ID will -- be this:GetName() -- -- returns: -- (Number delta, Number start, Number end) -- -- delta - the amount of time passed in seconds. -- start - the starting time -- now - the time the endTimer was called. --]] function Chronos.endTimer( id ) if ( not Chronos.online ) then return; end if ( not id ) then id = this:GetName(); end if ( not ChronosData.timers[id] or #ChronosData.timers[id] == 0) then return nil; end local now = GetTime(); -- Grab the last timer called local startTime = tremove(ChronosData.timers[id]); return (now - startTime), startTime, now; end --[[ -- getTimer([id]); -- -- Gets the timer and returns the amount of time passed. -- Does not terminate the timer. -- -- args: -- id - ID for the timer. If not specified, then ID will -- be this:GetName() -- -- returns: -- (Number delta, Number start, Number end) -- -- delta - the amount of time passed in seconds. -- start - the starting time -- now - the time the endTimer was called. --]] function Chronos.getTimer( id ) if ( not Chronos.online ) then return; end if ( not id ) then id = this:GetName(); end local now = GetTime(); if ( not ChronosData.timers[id] or #ChronosData.timers[id] == 0) then return 0, 0, now; end -- Grab the last timer called local startTime = ChronosData.timers[id][#ChronosData.timers[id]]; return (now - startTime), startTime, now; end --[[ -- isTimerActive([id]) -- returns true if the timer exists. -- -- args: -- id - ID for the timer. If not specified, then ID will -- be this:GetName() -- -- returns: -- true - exists -- false - does not --]] function Chronos.isTimerActive( id ) if ( not Chronos.online ) then return; end if ( not id ) then id = this:GetName(); end -- Create a table for this id's timers if ( not ChronosData.timers[id] ) then return false; end return true; end --[[ -- getTime() -- -- returns the Chronos internal elapsed time. -- -- returns: -- (elapsedTime) -- -- elapsedTime - time in seconds since Chronos initialized --]] function Chronos.getTime() return ChronosData.elapsedTime; end --[[ -- Chronos.afterInit(func, ...) -- Performs func after the game has truely started. -- By Thott --]] function Chronos.afterInit(func, ...) local id; if(this) then id = this:GetName(); else id = "unknown"; end --if(id == "SkyFrame") then -- Chronos.printError("Ignoring Sky init"); -- return; --end if(ChronosData.initialized) then func(...); else if(not ChronosData.afterInit) then ChronosData.afterInit = Chronos.getTable(); Chronos.schedule(0.2, Chronos.initCheck); end local recTable = Chronos.getTable(); recTable.func = func; recTable.args = Chronos.getArgTable(...); recTable.id = id; tinsert(ChronosData.afterInit, recTable); end end ------------------------------------------------------------------------------ --[[ Table Recycling ]]-- ------------------------------------------------------------------------------ function Chronos.getTable(...) local stack = Chronos.tables; if (not stack) then Chronos.tables = {}; stack = Chronos.tables; return {}; end local recTable; if (#stack >= 1) then recTable = tremove(stack) else recTable = {}; end for i=1, select("#", ...) do recTable[i] = select(i, ...); end return recTable; end -- Release a table to be nilled and used again. -- Optionally pass in an unpack(...) as the 2nd arg so that you can return the args: -- return Chronos.releaseTable(t1, unpack(t1)) function Chronos.releaseTable(t1, ...) if (type(t1) ~= "table") then return; end local stack = Chronos.tables; if (not stack) then Chronos.tables = {}; stack = Chronos.tables; end for k,v in pairs(t1) do t1[k] = nil; end tinsert(stack, t1); return ...; end ------------------------------------------------------------------------------ --[[ Helpers Functions ]]-- ------------------------------------------------------------------------------ function Chronos.getArgTable(...) if (select('#', ...) == 0) then return Chronos.emptyTable; else return Chronos.getTable(...); end end function Chronos.run(func,args) if(func) then if(args) then return func(unpack(args)); else return func(); end end end function Chronos.printError(text) ChatFrame1:AddMessage(text, RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b, 1.0, UIERRORS_HOLD_TIME); end function Chronos.printDebugError(var, text) if (var) and (getglobal(var)) then Chronos.printError(text); end end ------------------------------------------------------------------------------ --[[ Frame Script Helpers ]]-- ------------------------------------------------------------------------------ function Chronos.chatColorsInit() ChronosData.chatColorsInitialized = true; ChronosFrame:UnregisterEvent("UPDATE_CHAT_COLOR"); end function Chronos.initCheck() if(not ChronosData.initialized) then if(UnitName("player") and UnitName("player")~=UKNOWNBEING and UnitName("player")~=UNKNOWNBEING and UnitName("player")~=UNKNOWNOBJECT and ChronosData.variablesLoaded and ChronosData.enteredWorld and ChronosData.chatColorsInitialized) then ChronosData.initialized = true; Chronos.schedule(1,Chronos.initCheck); return; else Chronos.schedule(0.2,Chronos.initCheck); return; end end if(ChronosData.afterInit) then local i = ChronosData.afterInit_i; if(not i) then i = 1; end ChronosData.afterInit_i = i+1; --Chronos.printError("afterInit: processing ",i," of ",ChronosData.afterInit.n," initialization functions, id: ",ChronosData.afterInit[i].id); Chronos.run(ChronosData.afterInit[i].func, ChronosData.afterInit[i].args); if(i == #ChronosData.afterInit) then for i,v in ipairs(ChronosData.afterInit) do Chronos.releaseTable(v); end Chronos.releaseTable(ChronosData.afterInit); ChronosData.afterInit = nil; ChronosData.afterInit_i = nil; else Chronos.schedule(0.1, Chronos.initCheck); return; end end end --[[ -- Sends a chat command through the standard editbox --]] function Chronos.SendChatCommand(command) local text = ChatFrameEditBox:GetText(); ChatFrameEditBox:SetText(command); ChatEdit_SendText(ChatFrameEditBox); ChatFrameEditBox:SetText(text); end function Chronos.RegisterSlashCommands() --Needs to be able Variables load if you want to use Sky local chronosFunc = function(msg) local _,_,seconds,command = string.find(msg,"([%d\.]+)%s+(.*)"); if(seconds and command) then Chronos.schedule(seconds,Chronos.SendChatCommand,command); else Chronos.printError(SCHEDULE_USAGE1); Chronos.printError(SCHEDULE_USAGE2); end end if (Satellite) then Satellite.registerSlashCommand( { id = "Schedule"; commands = SCHEDULE_COMM; onExecute = chronosFunc; helpText = SCHEDULE_DESC; replace = true; } ); else SlashCmdList["CHRONOS_SCHEDULE"] = chronosFunc; for i = 1, #SCHEDULE_COMM do setglobal("SLASH_CHRONOS_SCHEDULE"..i, SCHEDULE_COMM[i]); end end end ------------------------------------------------------------------------------ --[[ Frame Scripts ]]-- ------------------------------------------------------------------------------ function Chronos.OnLoad() Chronos.framecount = 0; if (not ChronosData.byName) then ChronosData.byName = {}; end if (not ChronosData.repeating) then ChronosData.repeating = {}; end if (not ChronosData.sched) then ChronosData.sched = {}; end ChronosData.elapsedTime = 0; Chronos.afterInit(Chronos.RegisterSlashCommands); end function Chronos.OnEvent() if(event == "VARIABLES_LOADED") then ChronosData.variablesLoaded = true; ChronosFrame:Show(); elseif (event == "PLAYER_ENTERING_WORLD") then ChronosData.enteredWorld = true; Chronos.online = true; elseif (event == "PLAYER_LEAVING_WORLD") then Chronos.online = false; elseif ( event == "UPDATE_CHAT_COLOR" ) then Chronos.scheduleByName("ChronosAfterChatColorInit", 1, Chronos.chatColorsInit); end end function Chronos.OnUpdate_Quick() if ( not Chronos.online ) then return; end if ( not ChronosData.variablesLoaded ) then return; end if ( ChronosData.elapsedTime ) then ChronosData.elapsedTime = ChronosData.elapsedTime + arg1; else ChronosData.elapsedTime = arg1; end -- Execute scheduled tasks that are ready, pulling them off the front of the list queue. local now = GetTime(); local i; local task; while(#ChronosData.sched > 0) do if (not ChronosData.sched[1].time) then --Sea.io.printTable(ChronosData.sched[1]); tremove(ChronosData.sched, 1); elseif(ChronosData.sched[1].time <= now) then task = tremove(ChronosData.sched, 1); Chronos.run(task.handler, task.args); Chronos.releaseTable(task); else break; end end -- Execute named scheduled tasks that are ready. local k,v = next(ChronosData.byName); local newK, newV; while (k ~= nil) do newK,newV = next(ChronosData.byName, k); if (not v.time) then --Sea.io.printTable(v); ChronosData.byName[k] = nil; elseif (v.time <= now) then if (v.repeating) then ChronosData.byName[k].time = now + v.period; v.handler(); else Chronos.run(v.handler, v.args); Chronos.releaseTable(ChronosData.byName[k]); ChronosData.byName[k] = nil; end end k,v = newK,newV; end end function Chronos.OnUpdate_Debug() if ( not Chronos.online ) then return; end if ( not ChronosData.variablesLoaded ) then return; end local memstart = collectgarbage("count"); if ( ChronosData.elapsedTime ) then ChronosData.elapsedTime = ChronosData.elapsedTime + arg1; else ChronosData.elapsedTime = arg1; end local now = GetTime(); local i; local task; -- Execute scheduled tasks that are ready, popping them off the heap. while(#ChronosData.sched > 0) do if(ChronosData.sched[1].time <= now) then task = tremove(ChronosData.sched, 1); Chronos.run(task.handler, task.args); Chronos.releaseTable(task); else break; end end local memend = collectgarbage("count"); if(memend - memstart > 0) then Chronos.printError("gcmemleak from ChronosData.sched in OnUpdate: "..(memend - memstart)); end -- Execute named scheduled tasks that are ready. memstart = memend; local k,v = next(ChronosData.byName); local newK, newV; while (k ~= nil) do newK,newV = next(ChronosData.byName, k); if(v.time <= now) then local m = collectgarbage("count"); if (v.repeating) then ChronosData.byName[k].time = now + v.period; v.handler(); else Chronos.run(v.handler, v.args); Chronos.releaseTable(ChronosData.byName[k]); ChronosData.byName[k] = nil; end local mm = collectgarbage("count"); memstart = memstart + mm - m; end k,v = newK,newV; end memend = collectgarbage("count"); if(memend - memstart > 0) then Chronos.printError("gcmemleak from ChronosData.byName in OnUpdate: "..(memend - memstart)); end end ------------------------------------------------------------------------------ --[[ Frame Script Assignment ]]-- ------------------------------------------------------------------------------ --Event Driver if (not ChronosFrame) then CreateFrame("Frame", "ChronosFrame"); end ChronosFrame:Hide(); --Event Registration ChronosFrame:RegisterEvent("VARIABLES_LOADED"); ChronosFrame:RegisterEvent("PLAYER_ENTERING_WORLD"); ChronosFrame:RegisterEvent("PLAYER_LEAVING_WORLD"); ChronosFrame:RegisterEvent("UPDATE_CHAT_COLOR"); --Frame Scripts ChronosFrame:SetScript("OnEvent", Chronos.OnEvent); ChronosFrame:SetScript("OnUpdate", Chronos.OnUpdate_Quick); --OnLoad Call Chronos.OnLoad(); end