You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
12 KiB
316 lines
12 KiB
|
|
-- 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");
|