- Joined
- May 9, 2014
- Messages
- 1,819
A system that drastically improves the creation of custom events, with the added feature of semi-dynamic recursion depth for each EventListener object.
Along with this script, I've also added some test scripts to demonstrate its use and catch any run-time errors.
Documentation
Code
Addon
Changelog
Lua:
--[[
----------------------
-- EventListener --
-- v.1.0.3.1 --
----------------------
-- MyPad --
----------------------
----------------------------------------------------------------------
|
| About:
|
|----------------------------------------------------------------------
|
| EventListener is a class that enables a user to create a list
| of functions, which can be executed in sequence safely. It is
| designed to handle recursion depth logics, as well as edge case
| scenarios where functions are deregistered during the execution
| of the list of functions.
|
| By creating a list of functions, EventListener as a class is
| well suited to creating custom events, and responding to said
| events. For example, if one wishes to observe a damage event,
| but prevent the possibility of an infinite depth recursion,
| one can create an EventListener object that handles the depth
| recursion logic for the user.
|
| << Sample Code >>
| dmgEvent = EventListener:create()
| dmgEvent:setMaxDepth(8)
| << Sample Code >>
|
| The example above will create an EventListener that has a
| defined maximum recursion depth at 8. However, the code
| above will do nothing on its' own. Let's append it to the
| actual event.
|
| << Sample Code >>
| function SomeInitFunc()
| local t = CreateTrigger()
| TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DAMAGED)
| TriggerAddCondition(t, Condition(function()
| ...
| dmgEvent:run()
| end))
| end
|
| ...
| SomeInitFunc()
| << Sample Code >>
|
| As you can see, the dmgEvent will now execute every time
| a unit is damaged. This is achieved by calling
| EventListener:run(), which iterates through and calls
| the given list of functions.
|
| ----------------
| -- Features --
| ----------------
|
| - Recursion depth protection.
| - Robust function registration and removal.
|
|----------------------------------------------------------------------
| |-- API --|
|----------------------------------------------------------------------
|
| EventListener.create(o)
| EventListener.new(o) {LEGACY}
| EventListener(o) {LEGACY}
| - Returns a new EventListener object.
|
| EventListener:destroy()
| - Destroys a given EventListener object.
|
| EventListener:register(func) -> bool
| - Registers a given function to the
| EventListener object.
| - Registration may fail due to the following reasons:
| - The function may be already registered to the instance
| - The function may be blacklisted
| - The parameter may not be a function.
| - As of 1.0.3.0, <repetitions> field has been removed.
|
| EventListener:unregister(func)
| EventListener:deregister(func) {LEGACY}
| - Deregisters a function if found in
| the list of functions associated with the
| EventListener object.
|
| EventListener:destroy()
| - As of 1.0.3.0, if called while the EventListener
| object is still running, it will block subsequent
| executions and terminate the loop.
| - As of 1.0.3.0, the EventListener object removes
| all of its references to the tables it used.
|
| EventListener:cond_run(cond[, ...])
| EventListener:cfire(cond[, ...]) {LEGACY}
| EventListener:conditionalRun(cond[, ...]) {LEGACY}
| EventListener:conditionalExec(cond[, ...]) {LEGACY}
| - As of 1.0.3.0, the condition is evaluated only once.
| - Internally calls EventListener:run with
| the appended arguments (true, true, <...>).
|
| EventListener:run([, ...])
| EventListener:execute([, ...]) {LEGACY}
| EventListener:fire([, ...]) {LEGACY}
| EventListener:exec([, ...]) {LEGACY}
| - If an argument <...> is passed, it will be propagated
| to the body of executing functions. This has no
| practical limit. (Only restricted by Lua itself)
| - Destroys the object if it has been "finalized".
|
| EventListener:getMaxDepth(value)
| EventListener:getRecursionCount(value) {LEGACY}
| EventListener:getRecursionDepth(value) {LEGACY}
| - Returns the maximum amount of times a function can be executed
| recursively.
|
| EventListener:setMaxDepth(value)
| EventListener:setRecursionCount(value) {LEGACY}
| EventListener:setRecursionDepth(value) {LEGACY}
| - Sets the recursion depth to the given value.
| - A value of 0 or less will make the object
| susceptible to infinite loops.
|
| EventListener:getCallbackDepth(func)
| - Returns the integer depth of the requested function.
| - If the function isn't registered, this returns 0.
| - Note that very high values may suggest the presence
| of an infinite loop (typically around 3+).
|
| EventListener:getCurDepth()
| - Returns the integer depth of the EventListener instance.
| - If the current depth is greater than the maximum depth
| of the EventListener instance, all registered functions
| will not be executed.
|
| EventListener:getCurCallback()
| - Returns the currently executing function of the instance.
| - Defaults to DoNothing if the instance isn't currently running.
|
| EventListener:getCurCallbackIndex()
| - Returns the index of the currently executing function of the instance.
| - Defaults to 0 if the instance isn't currently running.
|
| EventListener:enable(func, flag)
| - Increments the "enabled" counter value of the registered
| function "func" by 1 if flag is true. Otherwise, decrements
| the "enabled" counter value by 1.
| - Returns a boolean value which reflects the success of the
| operation (with true corresponding to success and vice versa).
|
| EventListener:isEnabled(func)
| - If the function is not registered, this returns false.
| - If the "enabled" counter value is greater than 0, this
| returns true. Returns false otherwise.
|
----------------------------------------------------------------------
]]
Lua:
do
-- Determines the default amount of times a function can recursively be called
-- before the system treats it as a runaway infinite loop.
local _LOCAL_MAX_RECURSION = 8
-- Appends the current index of the executing function as the first parameter
-- among the list of parameters to be used. (Determined at compiletime)
local _APPEND_INDEX = false
-- This is actually author-exclusive and excludes all copies of
-- certain functions if set to false.
local _LEGACY_MODE = false
EventListener = {}
---@param baseFunc function -> The base function / method.
---@param baseName string -> The name of the base function / method.
---@param targetName string -> The name of the target function / method.
local function deprecatedFactory(baseFunc, baseName, targetName)
local print = print
return function(...)
print(targetName .. " has been deprecated. Use " .. baseName .. " instead.")
return baseFunc(...)
end
end
---@class EventListener
---@field private _funcList table -> Stores a list of functions registered to this object.
---@field private _funcMap table -> Stores a flag for each registered function.
---@field private _funcAbleCounter table -> Stores a counter that determines whether function is to be called when the EventListener runs.
---@field private _funcCallCounter table -> Stores a counter that determines the current recursion depth of the called function.
---@field private _funcMaxDepth integer -> Holds the maximum amount of times a function can be recursively called. Default is _LOCAL_MAX_RECURSION
---@field private _curDepth integer -> The current depth of the EventListener object in a recursive context.
---@field private _curFunc function -> The current executing function of the EventListener object.
---@field private _curIndex integer -> The current executing function's index of the EventListener object.
EventListener.__index = EventListener
EventListener.__metatable = EventListener
EventListener.__newindex =
function(t, k, v)
if EventListener[k] then return end
rawset(t, k, v)
end
EventListener.__len =
function(t)
return t._size or 0
end
if not IsFunction then
function IsFunction(func)
return type(func) == 'function'
end
end
local pcall = pcall
local DoNothing = DoNothing
---Creates a new instance.
---@param o table | nil
---@return EventListener
function EventListener.create(o)
o = o or {}
o._funcMap = {}
o._funcList = {}
o._funcAbleCounter = {}
o._funcCallCounter = {}
o._funcMaxDepth = _LOCAL_MAX_RECURSION
o._curDepth = 0
o._curIndex = 0
o._size = 0
o._curFunc = DoNothing
o._wantDestroy = false
setmetatable(o, EventListener)
return o
end
if _LEGACY_MODE then
EventListener.new = deprecatedFactory(EventListener.create, "EventListener.create", "EventListener.new")
EventListener.__call = deprecatedFactory(EventListener.create, "EventListener.create", "EventListener:__call")
end
---Destroys the instance.
function EventListener:destroy()
if (self._curDepth > 0) then
self._wantDestroy = true
return
end
self._funcMap = nil
self._funcList = nil
self._funcAbleCounter = nil
self._funcCallCounter = nil
self._funcMaxDepth = nil
self._curDepth = nil
self._curIndex = nil
self._curFunc = nil
self._wantDestroy = nil
self._size = 0
local mt = EventListener.__metatable
EventListener.__metatable = nil
setmetatable(self, nil)
EventListener.__metatable = mt
end
---Returns true if the requested function is
---already mapped within the queried instance.
---@param o EventListener - The queried instance
---@param func function - The requested function.
---@return boolean
local function alreadyRegistered(o, func)
return o._funcMap[func] ~= nil
end
---A list of blacklisted functions -> Cannot be registered as
---callback functions. If the function returns true, the parameter
---will not be registered. So far, only DoNothing is blacklisted.
---@param func function
---@return boolean
local function blacklistedFunction(func)
return func == DoNothing
end
---@param func function - The function to be registered.
---@return boolean - true if function was successfully registered; false otherwise.
function EventListener:register(func)
if (not IsFunction(func)) or
(alreadyRegistered(self, func) or
(blacklistedFunction(func))) then
return false
end
local index = #self._funcList + 1
self._size = self._size + 1
self._funcList[index] = func
self._funcAbleCounter[index] = 1
self._funcCallCounter[index] = 0
self._funcMap[func] = #self._funcList
return true
end
---@param func function - The function to be unregistered.
---@return boolean - true if function was successfully unregistered; false otherwise.
function EventListener:unregister(func)
if (not IsFunction(func)) or
(not alreadyRegistered(self, func) or
(blacklistedFunction(func))) then
return false
end
local i = self._funcMap[func]
self._funcList[i] = nil
self._funcAbleCounter[i] = nil
self._funcCallCounter[i] = nil
self._funcMap[func] = nil
self._size = self._size - 1
if (self._curFunc == func) then
self._curFunc = DoNothing
self._curIndex = 0
end
return true
end
if _LEGACY_MODE then
EventListener.deregister = deprecatedFactory(EventListener.unregister, "EventListener:unregister", "EventListener:deregister")
end
---This gets the smallest index higher than i
---that has a meaningful entry and maps the
---contents of the bigger index to the specified
---index i.
---@param i integer - The base index
---@param n integer - The size of the list as an explicit parameter.
---@param iBuffer integer - The buffer value to use for peeking.
---@return integer - Returns the updated value of iBuffer.
local function forwardSwap(self, i, n, iBuffer)
while (i + iBuffer <= n) do
if (self._funcList[i + iBuffer] ~= nil) then
local func = self._funcList[i + iBuffer]
self._funcList[i] = func
self._funcAbleCounter[i] = self._funcAbleCounter[i + iBuffer]
self._funcCallCounter[i] = self._funcCallCounter[i + iBuffer]
self._funcMap[func] = i
self._funcList[i + iBuffer] = nil
self._funcAbleCounter[i + iBuffer] = 0
self._funcCallCounter[i + iBuffer] = 0
break
end
iBuffer = iBuffer + 1
end
return iBuffer
end
---At compiletime, this function will either append the current
---index as part of the parameters or pass the parameters as
---they are.
local invokeFunction
if _APPEND_INDEX then
invokeFunction =
function(func, i, ...)
pcall(func, i, ...)
end
else
invokeFunction =
function(func, i, ...)
pcall(func, ...)
end
end
---Attempts to call all registered functions in sequential order.
---@vararg any optional parameters that can be any type. Passed down to callback functions.
function EventListener:run(...)
local i, n = 1, #self._funcList
local checkForDepth = (self._funcMaxDepth > 0)
local prevF, prevI = self._curFunc, self._curIndex
local iBuffer = 1
self._curDepth = self._curDepth + 1
while (i <= n) and (not self._wantDestroy) do
while (true) do
-- If the current index holds a recently deregistered function,
-- peek into future entries and place them at the current index.
if (self._funcList[i] == nil) then
iBuffer = forwardSwap(self, i, n, iBuffer)
-- Since there are no more entries, break the inner loop here.
if (i + iBuffer > n) then
break
end
end
if ((self._funcAbleCounter[i] < 1) or
(self._wantDestroy)) then
break
end
local func = self._funcList[i]
self._curIndex = i
self._curFunc = func
self._funcCallCounter[i] = self._funcCallCounter[i] + 1
if (not checkForDepth) or (self._funcCallCounter[i] <= self._funcMaxDepth) then
invokeFunction(func, i, ...)
end
if (self._wantDestroy) then
break
end
-- Since the list is mutable, consider the possibility that it
-- was the current function that was removed when the EventListener was
-- at a lower depth. If so, do not decrement.
if (func == self._curFunc) then
self._funcCallCounter[i] = self._funcCallCounter[i] - 1
else
iBuffer = forwardSwap(self, i, n, iBuffer)
n = #self._funcList
end
break
end
i = i + 1
end
self._curFunc, self._curIndex = prevF, prevI
self._curDepth = self._curDepth - 1
if (self._wantDestroy) and (self._curDepth <= 0) then
self:destroy()
return
end
if (not alreadyRegistered(self, self._curFunc)) then
self._curFunc = DoNothing
self._curIndex = 0
end
end
if _LEGACY_MODE then
EventListener.exec = deprecatedFactory(EventListener.run, "EventListener:run", "EventListener:exec")
EventListener.fire = deprecatedFactory(EventListener.run, "EventListener:run", "EventListener:fire")
EventListener.execute = deprecatedFactory(EventListener.run, "EventListener:run", "EventListener:execute")
end
---Evaluates the condition once before falling back to EventListener:run()
---@param cond function | boolean
---@vararg any optional parameters that can be any type. Passed down to callback functions.
function EventListener:cond_run(cond, ...)
if ((IsFunction(cond) and (not cond())) or
((not IsFunction(cond)) and (not cond))) then
return
end
self:run(...)
end
if _LEGACY_MODE then
EventListener.cfire = deprecatedFactory(EventListener.cond_run, "EventListener:cond_run", "EventListener:cfire")
EventListener.conditionalRun = deprecatedFactory(EventListener.cond_run, "EventListener:cond_run", "EventListener:conditionalRun")
EventListener.conditionalExec = deprecatedFactory(EventListener.cond_run, "EventListener:cond_run", "EventListener:conditionalExec")
end
---@param self EventListener - The EventListener object.
---@return integer - The max depth for a registered function within this instance.
function EventListener:getMaxDepth()
return self._funcMaxDepth
end
if _LEGACY_MODE then
EventListener.getRecursionCount = deprecatedFactory(EventListener.getMaxDepth, "EventListener:getMaxDepth", "EventListener:getRecursionCount")
EventListener.getRecursionDepth = deprecatedFactory(EventListener.getMaxDepth, "EventListener:getMaxDepth", "EventListener:getRecursionDepth")
end
---@param self EventListener - The EventListener object.
---@param i integer - The updated max depth value.
function EventListener:setMaxDepth(i)
self._funcMaxDepth = i
end
if _LEGACY_MODE then
EventListener.setRecursionCount = deprecatedFactory(EventListener.setMaxDepth, "EventListener:setMaxDepth", "EventListener:setRecursionCount")
EventListener.setRecursionDepth = deprecatedFactory(EventListener.setMaxDepth, "EventListener:setMaxDepth", "EventListener:setRecursionDepth")
end
---@param self EventListener - The EventListener object.
---@param func function - The function to be peeked.
---@return number - If not registered, defaults to 0. Returns the current depth of the function.
function EventListener:getCallbackDepth(func)
func = func or self._curFunc or DoNothing
if (not alreadyRegistered(self, func)) then
return 0
end
local i = self._funcMap[func]
return self._funcCallCounter[i]
end
---@param self EventListener - The EventListener object.
---@return integer - The current depth of the instance.
function EventListener:getCurDepth()
return self._curDepth or 0
end
---@param self EventListener - The EventListener object.
---@return function - The current callback function of the running instance.
function EventListener:getCurCallback()
return self._curFunc or DoNothing
end
---@param self EventListener - The EventListener object.
---@return integer - The index of the current callback function of the running instance.
function EventListener:getCurCallbackIndex()
return self._curIndex or 0
end
---@param self EventListener - The EventListener object.
---@param func function - The affected function
---@param flag boolean - The flag value.
---@return boolean - Defaults to false if function isn't registered. Returns true otherwise.
function EventListener:enable(func, flag)
if (not alreadyRegistered(self, func)) then
return false
end
local i = self._funcMap[func]
local j = (flag and 1) or -1
self._funcAbleCounter[i] = self._funcAbleCounter[i] + j
return true
end
---@param self EventListener - The EventListener object.
---@param func function - The affected function
---@return boolean - Defaults to false if function isn't registered. Returns true if counter value for function is greater than 0.
function EventListener:isEnabled(func)
if (not alreadyRegistered(self, func)) then
return false
end
local i = self._funcMap[func]
return self._funcAbleCounter[i] > 0
end
end
Lua:
if EventListener then
do
-- Here, DoNothing is actually doing something for the codebase
-- by being the default value in place of nil for function getters.
local DoNothing = DoNothing
---Gets the size of the list of functions.
---@return integer
EventListener.get_stack_size = EventListener.__len
---Gets the function at the requested index.
---Due to the possibility of the list being disjointed,
---this may have a worst case of a O(n) time complexity.
---@return integer
function EventListener:getf(index)
if (self._funcList[index] == nil) then
local iBuffer = 1
while (index + iBuffer <= #self._funcList) do
if (self._funcList[index + iBuffer] ~= nil) then
return self._funcList[index + iBuffer]
end
iBuffer = iBuffer + 1
end
return DoNothing
end
return self._funcList[index]
end
---Checks if the function is already registered to this
---Event Listener.
---@return boolean
function EventListener:is_function_in(func)
return self._funcMap[func] ~= nil
end
---Swaps the position of two functions within the list.
---Returns true if successful, false otherwise.
---@return boolean
function EventListener:swap(func1, func2)
if ((self._funcMap[func1] == nil) or
(self._funcMap[func2] == nil)) then
return false
end
local tempI = self._funcMap[func1]
local tempI2 = self._funcMap[func2]
local tempA, tempC = self._funcAbleCounter[tempI], self._funcCallCounter[tempI]
self._funcMap[func1] = tempI2
self._funcList[tempI] = func2
self._funcAbleCounter[tempI] = self._funcAbleCounter[tempI2]
self._funcCallCounter[tempI] = self._funcCallCounter[tempI2]
self._funcMap[func2] = tempI
self._funcList[tempI2] = func1
self._funcAbleCounter[tempI2] = tempA
self._funcCallCounter[tempI2] = tempC
if (self._curFunc == func1) then
self._curIndex = tempI2
elseif (self._curFunc == func2) then
self._curIndex = tempI
end
return true
end
end
elseif OnGlobalInit then
do
OnGameStart(
function()
print("Event Listener Add-ons >> You do not have EventListener installed.")
end)
end
else
do
TimerStart(CreateTimer(), 0.00, false,
function()
print("Event Listener Add-ons >> Please install Global Initialization first so that " ..
"the error message can properly be propagated.")
print("Link to Global initialization: https://www.hiveworkshop.com/threads/global-initialization.317099/")
end)
end
end
- v.1.0.0.0
- Release
- v.1.0.1.0
- Made the system extensible.
- Now features an addon script that allows one to get a function based on index, as well as swap the positions of certain functions.
- v.1.0.2.0
- Added a flag (IGNORE_INDEX) that tells the system to introduce the index of the currently executing function as the first argument.
- Note: This flag cannot change the system's behavior at runtime. It must be declared during compile-time.
- If true, the system will append the index of the currently executing function as the first argument.
- If false, the system will behave just like the previous version.
- Credits to AGD for the index suggestion.
- Fixed a potential bug with
is_function
where a table "function" might crash the thread because its metatable is inaccessible or does not exist.- If a metatable exists, the function is called recursively to the table's __call metamethod, returning the flag status of the metamethod.
- If a metatable does not exist, the system returns false.
- Added a flag (IGNORE_INDEX) that tells the system to introduce the index of the currently executing function as the first argument.
- v.1.0.3.0
- Rewrote the system while keeping backwards-compatibility as much as possible. There are some quirks, though:
- EventListener:register no longer accepts tables whose metatables have a __call metamethod.
- EventListener:deregister no longer accepts tables for the same reason.
- Added Emmy Notation to further express the intent of public functions, as well as their implementations.
- Added a Test Script section for the user to tinker with (optional).
- Spoiler tags now replaced with tabs.
- A link to the the repository can be found here: wc3/Lua/Events/Base at master · gitMyPad/wc3
- Rewrote the system while keeping backwards-compatibility as much as possible. There are some quirks, though:
- v.1.0.3.1
- Added a legacy flag to the system
- Documented the previously undocumented functions / methods.
- Added annotations to these functions / methods.
Along with this script, I've also added some test scripts to demonstrate its use and catch any run-time errors.
Parameter Propagation
Object creation, destruction, and infinite recursion prevention
Add-On Tests
Test Results
Lua:
OnGameInit(
function()
print("Game init >> Testing Event Listener")
local ev = EventListener.create()
print("Event Listener created")
ev:register(
function(x)
print("Square of x is " .. tostring(x*x))
end)
ev:register(
function(x)
print("x + 2 is " .. tostring(x + 2))
end)
print("Adder and Squaring function registered to Event Listener")
print("Running Event Listener with an input of 3")
ev:run(3)
print("Event Listener done (3)")
print("Running Event Listener with an input of 5")
ev:run(5)
print("Event Listener done (5)")
end)
Lua:
OnGameInit(
function()
print("=======================")
print("Game init >> Testing Event Listener (2)")
local ev = EventListener.create()
print("Event Listener (2) created. Restricting maximum depth to 2")
ev:setMaxDepth(2)
print("Max depth set for Event Listener (2)")
ev:register(
function(x)
print("This is my first message: " .. x, ev:getCurDepth())
print("Local depth of function: " .. tostring(ev:getCallbackDepth()))
end)
ev:register(
function(x)
print("This is my second message: " .. x, ev:getCurDepth())
ev:unregister(ev:getCurCallback())
print("Local depth of function: " .. tostring(ev:getCallbackDepth()))
end)
ev:register(
function(x)
print("This is my third message: " .. x, ev:getCurDepth())
print("Local depth of function: " .. tostring(ev:getCallbackDepth()))
end)
ev:register(
function(x)
print("This is my final message: " .. x, ev:getCurDepth())
print("Local depth of function: " .. tostring(ev:getCallbackDepth()))
ev:run(x)
end)
print("Mutability test of EventListener (2) now underway!")
print("Running Event Listener with an input of waku-waku")
ev:run("waku-waku")
print("Event Listener done (waku-waku)")
print("Running Event Listener with an input of NANI?!")
ev:run("NANI?!")
print("Event Listener done (NANI?!)")
ev:destroy()
print("Event Listener destroyed")
local result = pcall(
function()
print("Attempting to call unreachable methods")
ev:register(
function(x)
print("This is impossible to reach")
end)
end)
if (not result) then
print("Attempt failure, as expected.")
else
print("Attempt success?!! Watch out! Object is still not finalized!")
end
end)
Lua:
OnGameInit(
function()
print("=======================")
print("Game init >> Testing Event Listener (3)")
local ev = EventListener.create()
print("Event Listener (3) created. Testing enable and disable methods")
ev:setMaxDepth(2)
ev:register(
function()
print("First function (auto-disables itself)", ev:getCurDepth())
ev:enable(ev:getCurCallback(), false)
end)
ev:register(
function()
print("Second function (immediately calls ev:run() afterwards)", ev:getCurDepth())
ev:run()
end)
ev:register(
function()
print("Third function", ev:getCurDepth())
end)
print("Functions registered to Event Listener. Testing the enable function")
ev:run()
print("First trial concluded. Testing with first function disabled.")
ev:run()
print("Second trial concluded. Testing with second function disabled.")
ev:enable(ev:getf(1), true)
ev:enable(ev:getf(2), false)
ev:run()
ev:enable(ev:getf(2), true)
ev:enable(ev:getf(1), true)
print("Third trial concluded. Swapping first and third function.")
ev:swap(ev:getf(1), ev:getf(3))
ev:run()
print("Tests concluded")
end)
- Parameter Propagation
- The square of the input (3, 5) produced (9, 25).
- The sum of the input (3, 5) and 2 produced (5, 7).
- The input did not change between callback functions.
- Interpretation: Test Success!
- Object creation, destruction, and infinite recursion prevention
- Object creation and destruction behaved as intended.
- Second function in the list was removed.
- Fourth function in the list triggered an infinite recursion.
- Infinite recursion was intercepted and blocked, preventing the execution of the fourth function at depth 3.
- Interpretation: Test Success!
- Add-On Tests:
- swap and getf worked as intended.
- Interpretation: Test Success!
Last edited: