/* ====================================================================================================================== Task Manger - Event Management Table ====================================================================================================================== Purpose: All-in-one, single-file TaskManager for SRB2 (events/handlers/args/targets). This file includes documentation, changelog, examples, and a small SRB2-style self-test so it can be inspected or used standalone during development. NOTE: If you prefer a drop-in runtime module for direct inclusion in projects, download the core-only file ("L_TaskManager_core"), which contains only the runtime system (no docs, examples, or self-test). SUMMARY: - API: RegisterEvent, Register, Unregister, UnregisterByLocation, ClearEvents, ListEvents - See the example at the very end of this file. - Toggle SELFTEST = true to run the self-test on load. Set it to false before final release so it doesn’t clutter the console.. */ local VERSION = "1.0" local AUTHOR = "tep06" local COMPAT = "SRB2 v2.2.15" -- Normally I wouldn’t bother, but hey, let me boost my ego by pretending I made something amazing local SELFTEST = false -- Set true temporarily to run the quick test on load /* During its development ---------------------- This whole thing actually started as a small system to make SRB2 hooks less messy for my custom character. But somewhere along the way, I noticed it was way more flexible than I first thought. So now, it’s basically a Task Manager. Purpose ------- It allows you to: - Add a function to a specific event type (hook, update, etc.) for a specific target/entity (player, object, etc.). - Pass any number of custom arguments to that function. - Remove that function later (with the same arguments in the same order) to stop it. - Keep the exact execution order in which functions were added. Why use this instead of SRB2's default addHook ? ------------------------------------------------ This system mainly exists for 3 reasons: 1 - Cleaner code : Instead of having tons of addHook calls scattered everywhere, you only need one per event type, making it easier to maintain 2 - Better performance: It also voids unnecessary loops/iterations (like players.iterate, pairs, etc.) that can slow things down on low-end PCs. 3 - Everything in one place: All function registrations/removals are stored in a single global table. This means you can manage everything from another file, and your main code stays clean and easy to read. Can this system be used for something else than hooks or outside SRB2 ? ----------------------------------------------------------------------- Yes and no. The system is mainly designed for SRB2, but it’s flexible enough to handle other event-driven logic in Lua, like callbacks or task queues. However, keep in mind that SRB2 Lua uses some syntax or shortcuts that vanilla Lua doesn’t support natively. for example, the continue statement in loops, the "$" shortcut for reusing the last value, or even missing "then" in "if" statements, plus "!=" instead of "~=" for "not equal". If you want to use this system outside SRB2 or in plain Lua, you’ll need to remove or adapt those SRB2-specific constructs. Also, there might be existing Lua libraries out there that handle dynamic event/callback systems more robustly. It’s worth checking before reinventing the wheel. One more thing: I initially assumed SRB2 Lua had the debug library (like debug.traceback) for error tracking without stopping the whole program, but it’s not available. So, to show errors clearly, I had to fall back to simple error calls to keep stack traces. How to use: ----------- You have 8 functions at your disposal: #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 1 - TaskManager.Register(string eventName, /any except nil, upval, proto/ target, function task, varargs ...) #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : table Description : Adds a function to a specific event for a given target (player, mobj, number, string, etc.). You can pass custom variable-length arguments that will be provided to the function when it is called. If the specified event does not exist yet, it will be created automatically. Returns a table containing: - eventName : the name of the event - target : the target associated with the event - funcID : index of the registered function within eventData.Functions (used instead of storing the function directly to save memory) - argsID : index of the registered arguments within eventData.Arguments (used instead of storing the arguments directly to save memory) - queueIndex : current position of the call in the execution queue (__queue) Throws a console error if the argument types don’t match the expected format. \_______________________________________________________________________________________________________________/ #════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 2 - TaskManager.RegisterGiveHandle(string eventName, /any except nil, upval, proto/ target, function task, varargs ...) #════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : table Description: Same as TaskManager.Register, but also automatically stores the returned registration handle in an internal list before returning it. This prevents you from having to manually store the handle yourself each time and makes it easier to later remove or manage the registered call without needing to re-specify the original arguments. See TaskManager.Register for complete behavior details. \__________________________________________________________________________________________________________________________/ #════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 3 - TaskManager.UnregisterByLocation(string eventName, /any except nil, upval, proto/ target, int funcID, int argsID, int queueIndex) #════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Removes a previously added function from a specific event and target by directly using its stored IDs. Returns true if the task was successfully removed without any errors, and false if certain values could not be found (which can be fixed later). Throws a console error if the argument types don’t match the expected format. \______________________________________________________________________________________________________________________________________/ #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 4 - TaskManager.Unregister(string eventName, /any except nil, upval, proto/ target, function task, varargs ...) #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Removes a previously added function from a specific event and target. This method first finds the internal identifiers (funcID, argsID, queueIndex) corresponding to the given arguments, then calls TaskManager.UnregisterByLocation and returns its result. See TaskManager.UnregisterByLocation for complete behavior details. \_______________________________________________________________________________________________________________/ #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 5 - TaskManager.UnregisterFromHandle(table handle) #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Shorthand for TaskManager.UnregisterByLocation, automatically extracting all necessary parameters from the handle table returned by TaskManager.Register. See TaskManager.UnregisterByLocation for complete behavior details. \_______________________________________________________________________________________________________________/ #═════════════════════════════════════════════════════════════════════════════════════════════════════════# 6 - TaskManager.ClearEvents([/any except nil, upval, proto/ target]) #═════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Clears all events if called without arguments. If you pass a target, it only clears events tied to that target. Great for cleaning up when a player dies, respawns, or during map changes. The function returns true if the latter was successfully executed and false if the TaskManager is completely empty. However, it will throw a console error if the arguments don’t match the expected format. \_______________________________________________________________________________________________________/ #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# 7 - TaskMgr.ExecuteTask(string eventName, int funcID, int argsID) #═════════════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Executes a specific registered function identified by funcID using the arguments stored at argsID. This function is typically called internally by TaskManager.ExecuteEvent to run a single task within an event's task list. The function returns true if the task was executed successfully without any errors. The function returns false if it could not reach certain values that can be corrected later. However, it will throw a console error if the arguments don’t match the expected format. \_______________________________________________________________________________________________________________/ #═════════════════════════════════════════════════════════════════════════════════════════════════════════# 8 - TaskManager.ExecuteEvent(string eventName, [/any except nil, upval, proto/ target]) #═════════════════════════════════════════════════════════════════════════════════════════════════════════# return value : boolean Description : Executes all registered functions for a given event and target, if any. Otherwise, it executes all registered functions in the event. This function is called internally to run every event function with its stored arguments. The function returns true if all tasks in the event were executed successfully. The function returns false if it could not reach certain values that can be corrected later. However, it will throw a console error if the arguments don’t match the expected format. \_______________________________________________________________________________________________________/ Example (in this file below at the very end): --------------------------------------------- When the player picks up a ring, the player blinks, with two sparks rotating around them for each collected ring for 3 seconds. In addition to the visual effect, the player becomes invincible and faster. Each effect (blinking, sparks, invincibility, and speed boost) is handled by its own separate function. This separation is intentional to demonstrate 4 different methods of adding and removing effects within the TaskManager system. Of course, you should never do that in your own projects. Structure: ---------- --> To be clear: ------------ - Each event type has its own table You can also create your own tables, which is done automatically when storing a task in an unknown event e.g, you can have PostPlayerThink or PrePlayerThink if you want to do manipulations between each call in the same hook. - funcID/argsID = index of the variable in its specific table (ID for index) It avoids duplication of these variables TaskManagerData = { [eventName] = -- PlayerThink, ThinkFrame, MobjThinker, MobjCollide, etc.. Can be a custom one { -- Stocks every functions/tasks here Functions = {}, -- { [funcID] = functionRef } -- Stocks every arguments per table (e.g: argsTable1 = { arg1, arg2, arg3 }, argsTable2 = { arg1, arg2 }, etc) Arguments = {}, -- { [argsID] = argsTable } -- Stocks every index of a specified argsTable (e.g: [player] = {1, 2} -> refers to argsTable1 and 2 just above) -- Note : completely useless for the system, but could be useful for user projects managing multiple targets Targets = {}, -- { [target] = { argsIDs } } -- Preserves the execution order __queue = {}, -- { {target, funcID, argsID} } -- Allows us to know the index of the function relative to the Functions table more easily _nextFuncID = 0, -- Only used in the Add/Remove functions _nextArgsID = 0, -- Same as _nextFuncID but with arguments __queueCount = 0 -- Lenght of the __queue table } } Global Tables Used : -------------------- - RandomTable: A namespace (like "YuSonic" or "XSonic") for your custom functions. - TaskManagerData: The namespace for the task manager methods. Notes: ------ - This system is strictly individual meaning each user manages their own copy of the table, thus avoiding synchronization issues and reducing heavy load on the server if the table were global. - If you plan to use this system from this file in your mods: Avoid defining them as `local function` unless they are in the same file as the system. Otherwise, put them in a global or shared table so they are accessible (via RandomTable for example). - In this code, this system is used for all local players meaning you and the second player (if you are in split screen) - So if you want to use it for online play, DO NOT use the P_LocalPlayer(p) function as a target! Unless it doesn’t affect other players’ game state (like sounds or HUD), because it can cause serious desynchronization issues. In such cases, use more global checks instead (like the character’s skin). For example, if you want to use this system for a single character with a particular skin: change all "P_LocalPlayer(p)" conditions to "p and p.mo and p.mo.skin == "yourcharacter" depending on the context. I normally left them as comments in the code. Tips: ----- - If you’re new to programming, I don’t recommend using this system Because it involves managing multiple layers of function calls and dynamic arguments, which can quickly become confusing and hard to debug. It’s better to start with simpler structures before tackling something this flexible and complex, But I don't force you to. - Be careful not to keep references to things that no longer exist. For example, if you have a target Targ and it’s been removed, make sure to reset all its related data in the system. The same goes for any variables passed as arguments, whether they’re tables, userdata, functions, or anything Lua treats as a reference - In this file, I demonstrate four different methods for registering and unregistering a function. Most of the time, if you are not modifying the arguments of your function, simply using Unregister is perfectly fine. However, if in a certain context you need to modify one of the arguments making Unregister unusable, you should use UnregisterFromHandle together with RegisterGiveHandle (to avoid having to manually nilify the handle). In situations where multiple instances of the same function can be registered at once, a simple Unregister is also fine—it’s nothing to write home about. In fact, in such cases, storing IDs just to speed up removal can be a bit overkill; it’s better to save memory to avoid potential performance loss, The performance impact here is negligible in most cases. That said, since this system is more object-oriented, I’m not entirely sure how often this will actually be necessary, but I originally created this method to solve a specific problem so feel free to use it however you like. Shut up and give me the code ---------------------------- If you want to reuse this entire system in your own project, copy everything starting from the line where I placed a series of slashes (///////...) at the top, and stop when you reach the same series of slashes at the bottom. Or just download the latest version of "L_TaskManager_core" in the message board ====================================================================================================================== */ -- So let's begin ! --------------------------------------------------------- -- Global namespace for your custom functions -- (acts like a "mod namespace" to avoid polluting _G) --------------------------------------------------------- rawset(_G,"RandomTable",{}) -- Stores custom functions/tables local RndTable = RandomTable -- Local alias for faster access -- Start copying here ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -- Useful functions that we'll need for the system --------------------------------------------------------- -- Compat table.pack for Lua 5.1 (SRB2) --------------------------------------------------------- if not table.pack then table.pack = function(...) return { n = select("#", ...), ... } end end --------------------------------------------------------- -- Compare the content of two tables -- In our context, this will be used to compare two argument tables to find their index in the system. -- This way, the function doesn't need to perform a deep search -- Because if the args are tables/functions/userdata/etc., -- They will automatically be references, facilitating comparisons. --------------------------------------------------------- local function compareArgs(a, b) local na = a.n or #a local nb = b.n or #b if na ~= nb then return false end for i = 1, na do if b[i] ~= a[i] then return false end end return true end --------------------------------------------------------- -- Sends an error to the console with its traceback -- Used in functions to send a personalized message -- to the user to better understand his error --------------------------------------------------------- local function errorReturn(msg, noTrace) -- Unfortunately, SRB2 doesn't have a debug library, preventing us from seeing the traceback. -- Our only option is to use the error function itself. --msg = noTrace and $ or debug.traceback(msg, 2) -- Nooooo --CONS_Printf(consoleplayer, "\x82" .. msg) error(msg, 2) -- Unfortunately, this is our only way to know the stack traceback --return false end -- MAIN SYSTEM --------------------------------------------------------- -- Global namespace for the task manager -- Contains all the functions useful for manipulations --------------------------------------------------------- rawset(_G,"TaskManager",{}) local TaskMgr = TaskManager ------------------------------------------------------------------------ -- Task Manager storage ------------------------------------------------------------------------ local TaskManagerData ------------------------------------------------------------------------ -- Registers storage ------------------------------------------------------------------------ local _Register local _RegisterCount --------------------------------------------------------- -- Empty args table (shared reference to save memory) --------------------------------------------------------- local NO_ARGS = {} ----------------------------------------------------------------------------------------- -- Register a call to an event for a specific target (player, mobj, etc) ----------------------------------------------------------------------------------------- TaskMgr.Register = function(eventName, target, task, ...) -- Checking for potential errors if type(eventName) ~= "string" then errorReturn("Error while trying to save a task: invalid event name type (string excepted, got " .. type(eventName) .. ".") end local base_error_msg = "Error while trying to save a task from '" .. eventName .. "' event: " local targetType = type(target) if targetType == "upval" or targetType == "proto" or target == nil then errorReturn(base_error_msg .. "invalid target type (a ".. targetType .." value).") end if type(task) ~= "function" then errorReturn(base_error_msg .. "invalid task type (function excepted, got " .. type(task) .. ".") end -- Register a call -- Create the main table if it doesn't exist TaskManagerData = $ or {} -- Create an event if it doesn't exist TaskManagerData[eventName] = $ or {} local eventData = TaskManagerData[eventName] -- faster reference local args = (select("#", ...) == 0) and NO_ARGS or table.pack(...) -- select("#", ...) == 0 -> If there's nothing local funcID, argsID -- ID = index -- Create everything if the eventData is empty if next(eventData) == nil then eventData.Functions = {} eventData.Arguments = {} eventData.Targets = {} eventData.__queue = {} eventData._nextFuncID = 0 eventData._nextArgsID = 0 eventData.__queueCount = 0 else -- Find the IDs -- If args/functions are already present, do not duplicate them -- Functions/tasks for id, fn in pairs(eventData.Functions) do if fn == task then funcID = id break end end -- Arguments for id, argtbl in pairs(eventData.Arguments) do if compareArgs(argtbl, args) then argsID = id break end end end -- Create IDs if not already existing -- Functions/tasks if not funcID then eventData._nextFuncID = $ + 1 funcID = eventData._nextFuncID eventData.Functions[funcID] = task -- end -- Arguments if not argsID then eventData._nextArgsID = $ + 1 argsID = eventData._nextArgsID eventData.Arguments[argsID] = args -- Adding args end -- Create the target's table if not already existing eventData.Targets[target] = $ or {} -- Link args to the target table.insert(eventData.Targets[target], argsID) -- Keep order eventData.__queueCount = $ + 1 local queueCount = eventData.__queueCount local call = { target, funcID, argsID } eventData.__queue[queueCount] = call return {eventName = eventName, target = target, funcID = funcID, argsID = argsID, queueIndex = queueCount} end ----------------------------------------------------------------------------------------- -- Register a call to an event for a specific target -- Store and Return every info in _Register in a table ----------------------------------------------------------------------------------------- TaskMgr.RegisterGiveHandle = function(eventName, target, task, ...) local reg = TaskMgr.Register(eventName, target, task, ...) if not reg then return false end -- Returns a table containing everything about its location in the system _Register = $ or {} _RegisterCount = $ or 0 _RegisterCount = $ + 1 _Register[_RegisterCount] = reg return _Register[_RegisterCount] end ----------------------------------------------------------------------------------------- -- Unregister a call from an event by its location in the system ----------------------------------------------------------------------------------------- TaskMgr.UnregisterByLocation = function(eventName, target, funcID, argsID, queueIndex) if type(eventName) ~= "string" then errorReturn("Error while trying to remove a task by its location: invalid event name type (string expected, got " .. type(eventName) .. ").") end if not TaskManagerData then return false end local eventData = TaskManagerData[eventName] if not eventData or next(eventData) == nil then return false end if not eventData.Functions[funcID] or not eventData.Arguments[argsID] then return false end -- Remove in __queue (we check queueIndex if provided) if queueIndex and eventData.__queue[queueIndex] and eventData.__queue[queueIndex][1] == target and eventData.__queue[queueIndex][2] == funcID and eventData.__queue[queueIndex][3] == argsID table.remove(eventData.__queue, queueIndex) eventData.__queueCount = $ - 1 else -- The index is not valid anymore for i = #eventData.__queue, 1, -1 do local entry = eventData.__queue[i] if entry[1] == target and entry[2] == funcID and entry[3] == argsID then table.remove(eventData.__queue, i) eventData.__queueCount = $ - 1 break end end end -- If nothing in the queue, we completely remove the event if eventData.__queueCount == 0 then TaskManagerData[eventName] = nil else -- Remove in Targets local targets_list = eventData.Targets[target] if targets_list then for i = #targets_list, 1, -1 do if targets_list[i] == argsID then table.remove(targets_list, i) break end end -- FULL CLEAN: Remove function, targets and arguments if unused if next(targets_list) == nil then eventData.Targets[target] = nil end end local funcStillUsed, argsStillUsed for i = #eventData.__queue, 1, -1 do local entry = eventData.__queue[i] if entry[2] == funcID then funcStillUsed = true end -- The function/task is still used if entry[3] == argsID then argsStillUsed = true end -- The arguments are still used if funcStillUsed and argsStillUsed then break end -- stop early end -- Remove the function/task if not funcStillUsed then eventData.Functions[funcID] = nil end -- Remove the arguments if not argsStillUsed then eventData.Arguments[argsID] = nil end -- Remove from _Register if _Register for i = #_Register, 1, -1 do local reg = _Register[i] if reg.eventName == eventName and reg.target == target and reg.funcID == funcID and reg.argsID == argsID table.remove(_Register, i) _RegisterCount = $ - 1 break end end if next(_Register) == nil _Register = nil _RegisterCount = nil end end end -- Reset everything if next(TaskManagerData) == nil TaskManagerData = nil end -- The call has been successfully removed from the system return true end ----------------------------------------------------------------------------------------- -- Unregister a call from an event for a specific target ----------------------------------------------------------------------------------------- TaskMgr.Unregister = function(eventName, target, task, ...) -- Checking for potential failures if type(eventName) != "string" then errorReturn("Error while trying to remove a task: invalid event name type (string excepted, got " .. type(eventName) .. ".") end local base_error_msg = "Error while trying to remove a task from '" .. eventName .. "' event: " local targetType = type(target) if targetType == "upval" or targetType == "proto" or target == nil then errorReturn(base_error_msg .. "invalid target type (a ".. targetType .." value).") end if type(task) ~= "function" then errorReturn(base_error_msg .. "invalid task type (function excepted, got " .. type(task) .. ".") end if not TaskManagerData then return false end local eventData = TaskManagerData[eventName] if not eventData then -- errorReturn("Error while trying to remove a task: event '" .. eventName .. "' does not exist.") return false end if next(eventData) == nil then -- errorReturn(base_error_msg .. "the event is completely empty.") return false end -- Find funcID local funcID -- ID = index btw for id, fn in pairs(eventData.Functions) do if fn == task then funcID = id break end end if not funcID then -- errorReturn(base_error_msg .. "given task does not exist.") return false end -- Arguments local args = (select("#", ...) == 0) and NO_ARGS or table.pack(...) -- select("#", ...) == 0 -> If there's nothing -- Find argsID local argsID -- ID = index btw for id, argtbl in pairs(eventData.Arguments) do if compareArgs(argtbl, args) then argsID = id break end end if not argsID then -- errorReturn(base_error_msg .. "given arguments do not exist.") return false end -- Remove a call return TaskMgr.UnregisterByLocation(eventName, target, funcID, argsID) end ----------------------------------------------------------------------------------------- -- Unregister a call from an event by the procured table ----------------------------------------------------------------------------------------- TaskMgr.UnregisterFromHandle = function(handle) if type(handle) != "table" then errorReturn("Error while trying to remove a task from handle: invalid handle type (table excepted, got "..type(handle)..".") end if not (handle.eventName and handle.target and handle.funcID and handle.argsID) then -- queueIndex is not a problem if it is not there return false end return TaskMgr.UnregisterByLocation(handle.eventName, handle.target, handle.funcID, handle.argsID, handle.queueIndex) end ----------------------------------------------------------------------------------------- -- Clear all events (useful when restarting / respawning) ----------------------------------------------------------------------------------------- TaskMgr.ClearEvents = function(target) local targetType = type(target) if targetType == "upval" or targetType == "proto" then errorReturn("Error while trying to clear events: invalid target type (a ".. targetType .." value).") end if not TaskManagerData then return true end -- Reset everything if target == nil then TaskManagerData = nil _Register = nil _RegisterCount = nil else for eventName in pairs(TaskManagerData) do -- Reset everything related to the target local eventData = TaskManagerData[eventName] if next(eventData) == nil then continue end -- Remove the main table eventData.Targets[target] = nil -- __queue table if not eventData.__queue then continue end -- Remove each table containing the target for i = #eventData.__queue, 1, -1 do local entry = eventData.__queue[i] if entry[1] != target then continue end table.remove(eventData.__queue, i) end -- Clean the event if __queue is empty if next(eventData.__queue) == nil then TaskManagerData[eventName] = nil continue -- There's nothing left in this event end -- If there are still tables in __queue -- Remove unused functions/tasks & arguments in the main system -- Collect used funcIDs and argsIDs from __queue local usedFuncIDs = {} local usedArgsIDs = {} for i = #eventData.__queue, 1, -1 do local entry = eventData.__queue[i] local funcID = entry[2] local argsID = entry[3] usedFuncIDs[funcID] = true usedArgsIDs[argsID] = true end -- Remove unused Functions for funcID in pairs(eventData.Functions) do if not usedFuncIDs[funcID] then eventData.Functions[funcID] = nil end end -- Remove unused Arguments for argsID in pairs(eventData.Arguments) do if not usedArgsIDs[argsID] then eventData.Arguments[argsID] = nil end end -- Remove from _Register if _Register for i = #_Register, 1, -1 do local reg = _Register[i] if reg.target == target then table.remove(_Register, i) end end if next(_Register) == nil _Register = nil _RegisterCount = nil end end end if next(TaskManagerData) == nil then TaskManagerData = nil end end return true end ----------------------------------------------------------------------------------------- -- Execute one call of an event ----------------------------------------------------------------------------------------- TaskMgr.ExecuteTask = function(eventName, funcID, argsID) -- Checking for potential errors if type(eventName) != "string" then errorReturn("Error while trying to execute a tast: invalid event name type (string excepted, got " .. type(eventName) .. ".") end local base_error_msg = "Error while trying to execute a task from '" .. eventName .. "' event: " if type(funcID) != "number" then errorReturn(base_error_msg .. "invalid funcID type (a ".. type(funcID) .." value)") end if type(argsID) != "number" then errorReturn(base_error_msg .. "invalid argsID type (a ".. type(argsID) .." value)") end if not TaskManagerData then return false end local eventData = TaskManagerData[eventName] if not eventData then -- errorReturn("Error while trying to execute a task: event '" .. tostring(eventName) .. "' does not exist.") return false -- It is better to ignore than to put an error in this context because it could be very annoying end local fn = eventData.Functions[funcID] if not fn then -- errorReturn(base_error_msg .. "function with funcID '" .. tostring(funcID) .. "' does not exist.") return false end local args = eventData.Arguments[argsID] if not args then -- errorReturn(base_error_msg .. "arguments with argsID '" .. tostring(argsID) .. "' do not exist.") return false end -- Executing a task -- Function to call the task -- Call fn with no arguments if args equals NO_ARGS, -- otherwise unpack args as multiple arguments. -- Lua can't conditionally unpack arguments in one call, -- so fn must be called separately for each case. local function call_task() if args == NO_ARGS then return fn() end return fn(unpack(args, 1, args.n)) end call_task() -- Simply call the function/task -- No debug.traceback, no xpcall unfortunately /*-- Call the task in protected mode with xpcall local function stack_traceback(err) return debug.traceback(err, 2) end local ok, err = xpcall(call_task, stack_traceback) -- Log an error message to the console if the call failed "successfully" if not ok then errorReturn(err, true) end*/ -- The call was successfully executed return true end ----------------------------------------------------------------------------------------- -- Execute all calls of an event from a specific target ----------------------------------------------------------------------------------------- TaskMgr.ExecuteEvent = function(eventName, target) -- Checking for potential errors if type(eventName) != "string" then errorReturn("Error while trying to execute event: invalid event name type (string excepted, got " .. type(eventName) .. ".") end local base_error_msg = "Error while trying to execute event '" .. eventName .. "': " local targetType = type(target) if targetType == "upval" or targetType == "proto" then errorReturn(base_error_msg .. "invalid target type (a ".. targetType .." value).") end if not TaskManagerData then return false end local eventData = TaskManagerData[eventName] if not (eventData and eventData.__queue) then return false end -- Executing all tasks registered for the given target in this event -- snapshot to avoid issues if tasks unregister themselves local snapshot = {} for i = 1, #eventData.__queue do snapshot[i] = eventData.__queue[i] end -- Iterate over all entries in the event's __queue for _, entry in ipairs(snapshot) do if not target or entry[1] == target then local ok = TaskMgr.ExecuteTask(eventName, entry[2], entry[3]) if not ok then -- optionally log which task failed end end end -- All tasks executed successfully return true end -- Stop copying here ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ----------------------------------------------------------------------------------------- -- Some Infos + Selftest (AKA ignore) ----------------------------------------------------------------------------------------- TaskManager.VERSION = VERSION TaskManager.AUTHOR = AUTHOR TaskManager.COMPAT = COMPAT TaskManager.SELFTEST = SELFTEST -- ===== Selftest ===== local function _taskmanager_selftest() local ok, err = pcall(function() print("", "\x86" .. "TaskManager: Selftest start...") -- ensure clean state if TaskMgr.ClearEvents then TaskMgr.ClearEvents() end -- storage to check calls local calls = {} -- test handler: expects three numeric args, pushes their sum local function testHandler(a, b, c) table.insert(calls, (a or 0) + (b or 0) + (c or 0)) end -- 1) Register with RegisterGiveHandle and check ExecuteEvent triggers it local handle = TaskMgr.RegisterGiveHandle("TM_SELFTEST", "mockTarget", testHandler, 1, 2, 3) if not handle then error("RegisterGiveHandle failed") end -- run the event for that target if not TaskMgr.ExecuteEvent then error("ExecuteEvent missing") end TaskMgr.ExecuteEvent("TM_SELFTEST", "mockTarget") if #calls ~= 1 or calls[1] ~= 6 then error("ExecuteEvent did not call handler correctly (expected 6, got "..tostring(calls[1])..")") end -- 2) Unregister the handle and ensure the handler is no longer called if not TaskMgr.UnregisterFromHandle then error("UnregisterFromHandle missing") end local okUnreg = TaskMgr.UnregisterFromHandle(handle) if not okUnreg then error("UnregisterFromHandle returned false") end -- clear the record and call again calls = {} TaskMgr.ExecuteEvent("TM_SELFTEST", "mockTarget") if #calls ~= 0 then error("Handler still called after unregister") end -- 3) Put another registration, then clear all events and ensure nothing runs TaskMgr.Register("TM_SELFTEST", "mockTarget", testHandler, 4, 5, 6) TaskMgr.ClearEvents() calls = {} TaskMgr.ExecuteEvent("TM_SELFTEST", "mockTarget") if #calls ~= 0 then error("ClearEvents did not remove registrations") end print("\x83" .. "TaskManager: Selftest passed", "") end) if not ok then print("\x85" .. "TaskManager: Selftest FAILED -> " .. tostring(err), "") end end -- Start selftest if TaskManager.SELFTEST then _taskmanager_selftest() end -- ===== end of selftest ===== ----------------------------------------------------------------------------------------- -- Tasks execution ----------------------------------------------------------------------------------------- -- Function to display memory before and after an action if you want local function measureMemoryUsage() local memory = collectgarbage("count") -- CONS_Printf(p, "Memory : " .. memory .. " Ko") return memory end -- Return a player if the latter is local, nil otherwise -- Meaning you or the second player in splitscreen mode local function P_LocalPlayer(p) if not p then return end -- No player if p == consoleplayer then return p end -- local Player 1 if splitscreen and p == players[1] then return p end -- local Player 2 return end -- Clear events on map change -- Note : If you change map in the same gamemode (like warping to another level), -- The PlayerSpawn hook won't be called, so MapChange is here for us addHook("MapChange", function(map_id) TaskMgr.ClearEvents() end) -- We don't need to conserve functions after death addHook("MobjDeath", function(ded, sus) if not ded.player then return end --if ded.skin == "sonic" then if P_LocalPlayer(ded.player) then TaskMgr.ClearEvents(ded.player) end end, MT_PLAYER) addHook("PlayerThink", function(p) --if not (p and p.mo and p.mo.skin == "sonic") then return end if not (P_LocalPlayer(p) and p.mo) then return end p.register = $ or {} -- Store handle to allow unregistering later, used for method 2 TaskMgr.ExecuteEvent("PlayerThink", p) end) addHook("PostThinkFrame", do for p in players.iterate --if not (p and p.mo and p.mo.skin == "sonic") then continue end if not (P_LocalPlayer(p) and p.mo) then continue end local memory = measureMemoryUsage() TaskMgr.ExecuteEvent("PostThinkFrame", p) CONS_Printf(p, "\x84" .. "Memory difference : " .. (measureMemoryUsage() - memory) .. " Ko") end end) ----------------------------------------------------------------------------------------- -- Example: Player blinks and sparks spin around them for 3 seconds after picking a ring ----------------------------------------------------------------------------------------- -- Function to oscillate a value between min_value and max_value in a "ping-pong" manner -- Used for spark Z position local function PingPong(x, min_value, max_value) local range = max_value - min_value -- The range between the min and max values local period = range * 2 -- Full period (up and down cycle) local mod = (x - min_value) % period -- The remainder after subtracting min_value, wrapping around the period -- If the value is in the "upward" phase, return it normally within the range if mod < range then return min_value + mod else -- If the value is in the "downward" phase, reflect it back towards min_value return max_value - (mod - range) end end -- Blink player using Method 1 -- This method directly unregisters the function using the same arguments it was registered with. -- Pros: Very simple to write, no need to store a handle. -- Cons: Less flexible — changing arguments after registration will break unregistration. RndTable.BlinkPlayer = function(p, mo, timer) -- Remove the scheduled call when the timer expires or the player is hurt if timer.value <= 0 or P_PlayerInPain(p) then mo.flags2 = $ &~ MF2_SHADOW -- Online issues TaskMgr.Unregister("PostThinkFrame", mo.player, RndTable.BlinkPlayer, p, mo, timer) return -- Important: always stop code execution after unregistering at the top of the function end -- Toggle shadow flag for blinking effect -- Note: the engine resets this flag each frame, -- but online doesn't give a thing to other players -- so I do it the old way if timer.value % 3 == 0 then mo.flags2 = $ | MF2_SHADOW else mo.flags2 = $ &~ MF2_SHADOW end -- We do not decrement timer.value here because it is shared with the other functions end -- Make the player faster using Method 2 -- This method stores the registration handle returned by RegisterGiveHandle. -- The handle is automatically nilified upon unregistration, which saves memory management work. RndTable.Faster = function(p, mo, timer) if timer.value <= 0 or P_PlayerInPain(p) then p.normalspeed = skins[mo.skin].normalspeed TaskMgr.UnregisterFromHandle(p.register.faster_handle) return end -- Give Super Sneakers p.powers[pw_sneakers] = timer.value end -- Make the player invincible using Method 3 -- Similar to Method 2 but uses TaskMgr.Register instead of RegisterGiveHandle. -- This means we must manually nilify the handle after unregistration to free memory. RndTable.Invincible = function(p, mo, timer) if timer.value <= 0 or P_PlayerInPain(p) then TaskMgr.UnregisterFromHandle(p.register.invincible_handle) p.register.invincible_handle = nil -- Manual cleanup required return end -- Keep invincibility active for the remaining timer duration p.powers[pw_invulnerability] = timer.value end -- Spawn sparks around the player using Method 4 -- This method demonstrates how to handle multiple simultaneous effects of the same type. -- All handles are stored in a table (p.register.blink_handle), one per spark effect. -- We use Register instead of RegisterGiveHandle because we need custom cleanup logic to avoid table holes. RndTable.Sparks = function(p, mo, timer) if timer.value <= 0 or P_PlayerInPain(p) then -- Unregister the first handle in the table TaskMgr.UnregisterFromHandle(select(2, next(p.register.blink_handle))) -- Remove the first element to maintain a linear order and avoid nil gaps table.remove(p.register.blink_handle, 1) -- Free memory if no more spark effects remain if next(p.register.blink_handle) == nil then p.register.blink_handle = nil end return end -- Spawn two rotating sparks around the player for i = 0, 1 do local side = (i == 0 and ANGLE_90 or -ANGLE_90) + (ANG1 * timer.value) * 7 local x = FixedMul(cos(side), mo.radius) local y = FixedMul(sin(side), mo.radius) local z = PingPong(timer.value * FU, 0, mo.height) local spark = P_SpawnMobjFromMobj(mo, x, y, z, MT_SUPERSPARK) spark.scale = mo.scale / 2 end -- Decrement here timer.value = $ - 1 end -- Event execution: give all 4 effects when touching a ring addHook("TouchSpecial", function(special, mo) --if not (mo and mo.player and mo.skin == "sonic") then return end if not (mo and P_LocalPlayer(mo.player)) then return end local p = mo.player if special.type == MT_RING then local timer = { value = 3 * TICRATE } -- Method 1: Blink (direct argument-based unregistration) TaskMgr.Unregister("PostThinkFrame", p, RndTable.BlinkPlayer, p, mo, timer) -- Unregister if there's already the function, because we don't need several TaskMgr.Register("PostThinkFrame", p, RndTable.BlinkPlayer, p, mo, timer) -- Method 2: Speed boost (handle auto-nilified) if p.register.faster_handle then TaskMgr.UnregisterFromHandle(p.register.faster_handle) end p.register.faster_handle = TaskMgr.RegisterGiveHandle("PostThinkFrame", p, RndTable.Faster, p, mo, timer) -- Method 3: Invincibility (manual handle cleanup) if p.register.invincible_handle then TaskMgr.UnregisterFromHandle(p.register.invincible_handle) p.register.invincible_handle = nil end p.register.invincible_handle = TaskMgr.Register("PostThinkFrame", p, RndTable.Invincible, p, mo, timer) -- Method 4: Sparks (multiple instances stored in a table) p.register.blink_handle = $ or {} table.insert(p.register.blink_handle, TaskMgr.Register("PostThinkFrame", p, RndTable.Sparks, p, mo, timer) ) end end)