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

-- 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");