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.

314 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, tinsert, pcall, C_Timer_After =
wipe, math.max, tonumber, unpack, coroutine, type, select, tremove, tinsert, pcall, C_Timer.After;
local c_create, c_yield, c_resume, c_status
= coroutine.create, coroutine.yield, coroutine.resume, coroutine.status;
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, s, c;
for i=#Stack,1,-1 do
f, p = Stack[i], StackParams[i];
-- app.PrintDebug("StackCo:Run",i,f,p)
s, c = pcall(f, p);
-- Function call has an error or it is not continuing, remove it from the Stack
if not s or not c then
if not s then app.PrintDebug("StackError:",i,f,p,c) 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
app.PrintDebug("RunStack:Error:",err)
if app.Debugging then
local instanceTrace = debugstack(StackCo, err);
print(instanceTrace)
end
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
local _PushQueue = {};
-- 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.
-- 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",
-- any index reference will return the next available pusher
__index = function()
local pusher = _PushQueue[1];
if pusher then
tremove(_PushQueue, 1);
-- app.PrintDebug("PUSH:Cache",#PushQueue)
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
app.PrintDebug("PUSH.Run.Error",co)
-- Throw the error. Returning nothing is the same as canceling the work.
-- local instanceTrace = debugstack(instance);
if app.Debugging then
local instanceTrace = debugstack(co, err);
print(instanceTrace)
end
-- error(err,2);
-- print(debugstack(instance));
-- print(err);
-- app.report();
end
end
-- After the pusher is done running the coroutine, it can return itself to the cache
-- app.PrintDebug("PUSH:Return",pushfunc,"=>",#_PushQueue + 1)
_PushQueue[#_PushQueue + 1] = pushfunc;
-- 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,"Qi",QueueIndex,"Ri",RunIndex,"#F",#FunctionQueue)
SetPerFrame(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)
-- app.PrintDebug("FR:Reset."..name,"Qi",QueueIndex,"Ri",RunIndex,"#F",#FunctionQueue)
end
-- Static coroutine for the Runner which runs one loop each time the Runner is called, and yields on the Stack
local 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))
pcall(func, unpack(params));
else
-- app.PrintDebug("FRC.Run.1."..name,RunIndex,ParameterSingleQueue[RunIndex])
pcall(func, ParameterSingleQueue[RunIndex]);
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);
-- Static Function that handles the Stack-Run for the Runner-Coroutine
local function StackRun()
-- app.PrintDebug("Stack.Run",Name)
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
app.PrintDebug("Stack.Run.Error",Name)
-- Throw the error. Returning nothing is the same as canceling the work.
if app.Debugging then
local instanceTrace = debugstack(RunnerCoroutine);
print(instanceTrace)
end
error(err,2);
-- print(debugstack(instance));
-- print(err);
-- app.report();
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,
};
-- 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
return Runner;
end
app.CreateRunner = CreateRunner;