• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Event Listener

A system that drastically improves the creation of custom events, with the added feature of semi-dynamic recursion depth for each EventListener object.

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.
  • 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
  • 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:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
This is nice. Btw, the ability to pass arguments to the callback thru execute() and conditionalExec() is not written in the documentation. I was about to suggest the feature but realized it's already supported - just not written in the docs.
Additional suggestion: I think you should pass the index of the current executing function as the first argument before the .... This could be useful since your add-on script also has a feature for accessing functions in the stack based on index.
 
Last edited:
This is nice. Btw, the ability to pass arguments to the callback thru execute() and conditionalExec() is not written in the documentation. I was about to suggest the feature but realized it's already supported - just not written in the docs.
Additional suggestion: I think you should pass the index of the current executing function as the first argument before the
...
. This could be useful since your add-on script also has a feature for accessing functions in the stack based on index.

Nearly forgot about this version (have since used a more requirement-based version of my EventListener). I might consider revising a part of the API to accommodate some undocumented changes.

I might have some mixed feelings about the next suggestion, though I'll update the system nonetheless to accommodate it.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Approved as one of multiple options of processing events in Lua. Having GUI variable event support would be nice, so I might add 3 separate flavors to my own event library (strictly my version, then a flavor that adds GUI support to yours, then another that adds GUI support to @ScorpioT1000 's eventDispatcher).
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
It seems to me that, while there's a documentation section, there should be the complete emmy annotation on the code section. What I mean is, until the local function blacklistedFunction you provided a full in depth emmy annotation, but then on EventListener:register seems incomplete, and subsequently EventListener:deregister has nothing. Even though they're pretty obvious, why should these have nothing or be inconsistent with the rest? What I can deduce from the looks of it is that it's sort of incomplete in that way.
Furthermore, I don't see why there has to be 4 or 5 field names that do the same thing, that can be rather polluted and confusing at times, just choose one that is the most intuitive and concise. For example, everyone knows what dmgEv:run() does, but I certainly would have difficulty to know what object:cfire() would ever do.

Lastly, getCallbackDepth and getCurCallback and a few others aren't mentioned on documentation either.
What is EventListener:c?

Going through all that, this resource is a must have for me :)
 
[...]

Lastly, getCallbackDepth and getCurCallback and a few others aren't mentioned on documentation either.
What is EventListener:c?
Cool find with the undocumented functions. I've now documented them in an upcoming version. EventListener:c, if I recall, is supposed to refer to cond_run in the documentation.

[...], I don't see why there has to be 4 or 5 field names that do the same thing, that can be rather polluted and confusing at times, just choose one that is the most intuitive and concise. For example, everyone knows what dmgEv:run() does, but I certainly would have difficulty to know what object:cfire() would ever do.
Those are artifacts from previous versions (I tend to rewrite these systems a lot, mostly for personal uses). As such, I've included a legacy option in the upcoming version which will notify users that may be using the alternative functions to use the mainline function instead.

EDIT:
New version live:

v.1.0.3.1

  • Added a legacy flag to the system
  • Documented the previously undocumented functions / methods.
    • Added annotations to these functions / methods.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
After a quick glance, looks really great.
By the way, if you want to have an optional parameter you can use the symbol ? after the parameter name.
For example:
Lua:
    ---Creates a new instance.
    ---@param o? table
    ---@return EventListener
    function EventListener.create(o)
It really has no different effect, just can be simpler to understand at first sightreading. Not really a big deal.
 
Top