-- Runner Lib local _, app = ...; -- Concepts: -- Capability to add to and run an entire set of Functions each frame, removing those which do not return a status -- Capability to add and run coroutine Functions, with one loop of iteration per frame -- Capability to add to and run a sequence of Functions with a specific allotment being processed individually each frame -- Global locals local wipe, math_max, tonumber, unpack, coroutine, type, select, tremove, pcall, C_Timer_After = wipe, math.max, tonumber, unpack, coroutine, type, select, tremove, pcall, C_Timer.After; local c_create, c_yield, c_resume, c_status = coroutine.create, coroutine.yield, coroutine.resume, coroutine.status; local function PrintError(err, source, co) print("ERROR:",source,":",err) if co and app.Debugging then local instanceTrace = debugstack(co); print(instanceTrace) end end local Stack = {}; local StackParams = {}; -- Tracks whether the Stack has already been requested to begin running local RunningStack; -- Function that queues RunStack only once regardless of call-count within one frame local QueueStack; -- A static coroutine which can be invoked to reverse-sequentially process all Functions within the Stack, -- passing the corresponding Stack param to each called Function. -- Any Functions which do not return a status will be removed local StackCo local function SetStackCo() -- app.PrintDebug("SetStackCo") StackCo = c_create(function() while true do -- app.PrintDebug("StackCo:Call",#Stack) local f, p, status, err; for i=#Stack,1,-1 do f, p = Stack[i], StackParams[i]; -- app.PrintDebug("StackCo:Run",i,f,p) status, err = pcall(f, p); -- Function call has an error or it is not continuing, remove it from the Stack if not status or not err then if not status then PrintError(err, "StackCo", StackCo) end -- app.PrintDebug("StackCo:Remove",i) tremove(Stack, i); tremove(StackParams, i); end end -- app.PrintDebug("StackCo:Done",f,p) -- Re-call StackCo if anything remains in the Stack if #Stack > 0 then -- app.PrintDebug("StackCo:QueueStack",#Stack) QueueStack(); end -- after processing the Stack, yield this coroutine -- app.PrintDebug("StackCo:Yield") c_yield(); end end) end SetStackCo() -- Function that begins a once-per-frame pass of the StackCo to run all Functions in the Stack local function RunStack() -- app.PrintDebug("StackCoStatus:",c_status(StackCo)) if c_status(StackCo) == "dead" then SetStackCo() end RunningStack = nil; local ok, err = pcall(c_resume, StackCo); if not ok then PrintError(err, "RunStack", StackCo) end end QueueStack = function() -- app.PrintDebug("QueueStackStatus:",RunningStack and "REPEAT" or "FIRST",c_status(StackCo)) if RunningStack then return; end RunningStack = true; C_Timer_After(0, RunStack); end -- Accepts a param and Function which will execute on the following frame using the provided param local function Push(param, name, func) Stack[#Stack + 1] = func; StackParams[#StackParams + 1] = param or 1; -- app.PrintDebug("Push @",#StackParams,name,func,param) QueueStack(); end app.Push = Push; -- Represents a key-weak table containing a cache of functions which are desired to be run within a coroutine. -- If the function is temporary and all references are removed externally, then the respective cache entry can be removed as well when garbagecollection -- happens to run. This makes sure that we don't permanently hold references to every coroutined-function during the lifetime of the client local CoroutineCache = setmetatable({}, { __mode = "k", __index = function(t, func) if type(func) ~= "function" then return end -- Coroutines are typically not designed to be re-run, so wrap the func in a permanent loop so it can simply be restarted -- when retrieved from the cache instead of needing to be re-created each time it is used local co = c_create(function() while true do func(); c_yield(false); end end); -- app.PrintDebug("CO:New",co,"<==",func) t[func] = co; return co; end }); local function GetCoroutine(func, name) local co = CoroutineCache[func]; -- Mark this name/coroutine until the coroutine is returned CoroutineCache[name] = true; CoroutineCache[co] = name; return co; end -- Allows freeing a coroutine and the respective name used to create it initially local function ReturnCoroutine(co) local name = CoroutineCache[co]; -- app.PrintDebug("CO:Return",name,co) CoroutineCache[name] = nil; CoroutineCache[co] = nil; end -- We will make this a weak-value cache, such that the Push methods can be cleaned up/recreated if needed local _PushQueue = setmetatable({}, {__mode = "v",}) -- Represents a small set of Push functions which are used to allow the Stack to handle coroutine processing. As these functions have no bearing -- on the coroutine they run, they can be reused and only created when the current amount is not enough to handle all concurrent coroutines. local PushQueue = setmetatable({}, { -- any index reference will return the next available pusher __index = function() local pusher = _PushQueue[#_PushQueue]; if pusher then -- app.PrintDebug("PUSH:Cache",#_PushQueue) _PushQueue[#_PushQueue] = nil; return pusher; end -- app.PrintDebug("PUSH:New",#_PushQueue + 1) local function pushfunc(co) -- Check the status of the coroutine -- app.PrintDebug("PUSH:Run",pushfunc,"=>",co) if co and c_status(co) ~= "dead" then local ok, err = c_resume(co); if ok then if err == false then -- This means the coroutine signals completion by returning false -- app.PrintDebug("PUSH.Run.Complete",co) else -- This means more work is required. -- app.PrintDebug("PUSH.Run.Yielded",co) return true; end else PrintError(err, "PUSH.Run", co) end end -- After the pusher is done running the coroutine, it can return itself to the cache _PushQueue[#_PushQueue + 1] = pushfunc; -- app.PrintDebug("PUSH:Return",pushfunc,"=>",#_PushQueue) -- Then grab the corresponding Name of this coroutine based on the coroutine cache -- and swap in the coroutine for the Name, and un-flag the Name from the NameCache ReturnCoroutine(co); end; return pushfunc; end }); -- Allows running a function on a coroutine until it completes local function StartCoroutine(name, func, delay) if not func or CoroutineCache[name] then return; end -- app.PrintDebug("CO:Prep",name); local co = GetCoroutine(func, name); local pusher = PushQueue.Next; if delay and delay > 0 then -- app.PrintDebug("CO:Delay",delay,name,pusher,co); C_Timer_After(delay, function() Push(co, name, pusher) end); else -- app.PrintDebug("CO:Start",name,pusher,co); Push(co, name, pusher); end end app.StartCoroutine = StartCoroutine; -- Iterative Function Runner -- Creates a Function Runner which can execute a sequence of Functions on a set iteration per frame update local function CreateRunner(name) local FunctionQueue, ParameterBucketQueue, ParameterSingleQueue, Config = {}, {}, {}, { PerFrame = 1 }; local Name = "Runner:"..name; local QueueIndex, RunIndex = 1, 1 local Pushed, perFrame local function SetPerFrame(count) Config.PerFrame = math_max(1, tonumber(count) or 1); -- app.PrintDebug("FR.PerFrame."..name,Config.PerFrame) -- always yield immediately so that it takes effect when encountered perFrame = 0 end local function Reset() -- app.PrintDebug("FR:Reset."..name,Pushed and "RUNNING" or "STOPPED","Qi",QueueIndex,"Ri",RunIndex,"@",Config.PerFrame) SetPerFrame(Config.PerFrameDefault or 1) -- when done with all functions in the queue, reset the indexes and clear the queues of data QueueIndex = 1 RunIndex = Pushed and 0 or 1 -- reset while running will resume and continue at index 1 wipe(FunctionQueue) wipe(ParameterBucketQueue) wipe(ParameterSingleQueue) end local function Stats() app.print(name,Pushed and "RUNNING" or "STOPPED","Qi",QueueIndex,"Ri",RunIndex,"@",Config.PerFrame) end -- Static coroutine for the Runner which runs one loop each time the Runner is called, and yields on the Stack local RunnerCoroutine local SetRunnerCoroutine = function() RunnerCoroutine = c_create(function() while true do perFrame = Config.PerFrame local params; local func = FunctionQueue[RunIndex]; -- app.PrintDebug("FRC.Running."..name) while func do perFrame = perFrame - 1; params = ParameterBucketQueue[RunIndex]; if params then -- app.PrintDebug("FRC.Run.N."..name,RunIndex,unpack(params)) local ok, err = pcall(func, unpack(params)); if not ok then PrintError(err, "Run."..Name) end else -- app.PrintDebug("FRC.Run.1."..name,RunIndex,ParameterSingleQueue[RunIndex]) local ok, err = pcall(func, ParameterSingleQueue[RunIndex]); if not ok then PrintError(err, "Run."..Name) end end -- app.PrintDebug("FRC.Done."..name,RunIndex) if perFrame <= 0 then -- app.PrintDebug("FRC.Yield."..name) c_yield(); perFrame = Config.PerFrame; end RunIndex = RunIndex + 1; func = FunctionQueue[RunIndex]; end -- Run the OnEnd function if it exists local OnEnd = FunctionQueue[0]; if OnEnd then -- app.PrintDebug("FRC.End."..name,#FunctionQueue) OnEnd(); end Pushed = nil; Reset(); -- Yield false to kick the StackRun off the Stack to stop calling this coroutine since it is complete until Run is called again c_yield(false); end end); -- app.PrintDebug("SetRunnerCoroutine",Name) end SetRunnerCoroutine() -- Static Function that handles the Stack-Run for the Runner-Coroutine local function StackRun() -- app.PrintDebug("Stack.Run",Name) if c_status(RunnerCoroutine) == "dead" then SetRunnerCoroutine() end local ok, err = c_resume(RunnerCoroutine); if ok then if err == false then -- app.PrintDebug("Stack.Run.Complete",Name) return; -- This means the coroutine signals completion by returning false else -- app.PrintDebug("Stack.Run.Yielded",Name) return true; -- This means more work is required. end else PrintError(err, Name, RunnerCoroutine) end end -- Provides a utility which will process a given number of functions each frame in a Queue local Runner = { -- Adds a function to be run with any necessary parameters Run = function(func, ...) if type(func) ~= "function" then error("Must be a 'function' type!") end FunctionQueue[QueueIndex] = func; -- app.PrintDebug("FR.Add."..name,QueueIndex,...) local arrs = select("#", ...); if arrs == 1 then ParameterSingleQueue[QueueIndex] = ...; elseif arrs > 1 then ParameterBucketQueue[QueueIndex] = { ... }; end QueueIndex = QueueIndex + 1; -- Only push the coroutine onto the Stack once until it is completed if Pushed then return; end Pushed = true; Push(nil, Name, StackRun); end, -- Set a function to be run once the queue is empty. This function takes no parameters. OnEnd = function(func) FunctionQueue[0] = func; end, -- Return the current PerFrame of the Runner GetPerFrame = function() return Config.PerFrame end, -- Return if the Runner is currently Running IsRunning = function() return Pushed end, -- Allows defining the default PerFrame for this Runner (i.e. when Reset) SetPerFrameDefault = function(count) Config.PerFrameDefault = count end }; -- Defines how many functions will be executed per frame. Executes via the Runner when encountered in the Queue, unless specified as 'instant' Runner.SetPerFrame = function(count, instant) if instant then SetPerFrame(count); else Runner.Run(SetPerFrame, count); end end Runner.Reset = Reset -- for testing Runner.Stats = Stats -- for testing app.Runners[name] = Runner return Runner; end -- Retrieves an existing or creates a new Runner with the provided name app.CreateRunner = function(name) return app.Runners[name] or CreateRunner(name) end app.Runners = {} app.FunctionRunner = CreateRunner("default");