• 🏆 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 [The Future of GUI Events]

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
You've seen Event, Event, Event, Event Listener, Event Dispatcher, Lua Variable Event, Pseudo Var Event, Evaluate Code, but you've not seen:

Event

Welcome to the future of event handling.
  • Automatic recursion prevention, as introduced in Damage Engine 5.
  • Automatic integration of Lua events with GUI.
  • Simple event creation, registration and execution.
  • Powerful, feature-rich and efficient.

Lua:
OnInit(function()
    Require "Event" --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Event.lua

    Event.create("event1")
    Event.create("event2")

    Event.event1.register(function(x)
        print( "first function: ".. x)
        Event.event2.await(function(y) print( "first function pt 2: ".. y) end)
    end)
    Event.event1.register(function(x)
        print( "second function: ".. x)
        Event.event2.await(function(y) print( "second function pt 2: ".. y) end)
    end)

    Event.event1("main seq")
    Event.event1("second seq")
 
    Event.event2("main seq")
    Event.event2("second seq")
end)

Prints:

1668588224026.png





Lua:
--Event creation and execution can only be done using Lua.
OnInit(function()

    Require "Event" --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Event.lua

    Event.create "BasicEvent1"
    Event.create "BasicEvent2"
    Event.create "BasicEvent3"

    OnInit.final(function()

        local eventId1={}
        local eventId2={}
        local eventId3={}

        Event.BasicEvent1(eventId1)
        Event.BasicEvent1(eventId2)
        Event.BasicEvent1(eventId3)
 
        Event.BasicEvent2(eventId2)
        Event.BasicEvent2(eventId1)
        Event.BasicEvent2(eventId3)

        Event.BasicEvent3(eventId3)
        Event.BasicEvent3(eventId2)
        Event.BasicEvent3(eventId1)
    end)
end)

  • GUI Test
    • Events
      • Game - BasicEvent1 becomes Equal to 0.00
    • Conditions
    • Actions
      • Set VariableSet MyInteger = (MyInteger + 1)
      • Set VariableSet IntegerArray[EventIndex] = MyInteger
      • Game - Display to (All players) for 60.00 seconds the text: (Holding until next event + (String(IntegerArray[EventIndex])))
      • Set VariableSet WaitForEvent = BasicEvent2
      • Game - Display to (All players) for 60.00 seconds the text: (Second event runs out of order: + (String(IntegerArray[EventIndex])))
      • Set VariableSet WaitForEvent = BasicEvent3
      • Game - Display to (All players) for 60.00 seconds the text: (Third event runs in reverse: + (String(IntegerArray[EventIndex])))
Prints:

1662059185722.png




Lua:
--[[
    Event v2.1

    Event is built for GUI support, event linking via coroutines, simple events (e.g. Heal Event),
    binary events (like Unit Indexer) or complex event systems like Spell Event, Damage Engine and Unit Event.

    Event.create
    ============
    Create an event that is recursion-proof by default, with easy syntax for GUI support.

    --In its most basic form:
    Event.create "MyEvent"          -> Create your event.
    Event.MyEvent.register(myFunc)  -> Global API for the user to call to register their callback function.
    Event.MyEvent()                 -> call this to execute the event (inherits this functionality from Hook).
   
    --If GUI has a variable by the same name, it hooks it internally (automating the udg_ portion) to allow this to work:
    Game - Value of MyEvent becomes Equal to 0.00

    NOTE - the value that MyEvent compares to is its priority in the event sequence, so events with higher numbers run first.

    --Enhanced event execution:
    Event.MyEvent.execute(extraValue:any, eventSucceeded:boolean, ...)
        - Run the event with special data attached (e.g. Spell Event uses the ability ID, Damage Engine uses limitops)
        - In most cases, eventSucceeded should be "true". However (for example) Attack Engine -> Damage Engine data transmission will use "false" to cover "missed" events.
        - Neither extraValue nor eventSucceeded are propogated as parameters to the callback functions.
   
    --Enhanced event registration:
    Event.SpellEffect.await(function() print "Medivh's Raven Form was used" end), 'Amrf', true)
        - This is an example of how Spell Event uses the ability ID to distinguish a special callback to this function.
        - The second parameter specifies the value that should be matched for the event to run.
        - The third value must be "true" if the event should be static (rather than called only once)
   
    --WaitForEvent functionality:
    Event.OnUnitIndexed.register(function()
        print"Unit Indexed" --runs for any unit.
        Event.OnUnitRemoval.await(function()
            print "Unit Deindexed" --runs only for the specific unit from the OnUnitIndexed event, and then automatically removes this one-off event once it runs.
        end)
    end)
]]
OnInit(function(require) --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Total_Initialization.lua

    local hook        = require "AddHook"                --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Hook.lua
    local remap       = require.lazily "GlobalRemap"     --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Global_Variable_Remapper.lua
    local sleep       = require.lazily "PreciseWait"     --https://github.com/BribeFromTheHive/Lua-Core/blob/main/PreciseWait.lua
    local wrapTrigger = require.lazily "GUI.wrapTrigger" --https://github.com/BribeFromTheHive/Lua-Core/blob/main/Lua-Infused-GUI.lua

    local _PRIORITY   = 1000 --The hook priority assigned to the event executor.
   
    local allocate, continue
    local currentEvent, depth = {}, {}

    Event = {
        current = currentEvent,
        stop    = function() continue = false end
    }

    do
        local function addFunc(name, func, priority, hookIndex)
            assert(type(func)=="function")
            local funcData = {active = true}
            funcData.next,
            funcData.remove = hook(
                hookIndex or name,
                function(...)
                    if continue then
                        if funcData.active then
                            currentEvent.funcData = funcData
                            depth[funcData] = 0
                            func(...)
                        end
                        funcData.next(...)
                    end
                end,
                priority, (hookIndex and Event[name].promise) or Event, DoNothing, false
            )
            return funcData
        end

        ---@param name string        -- A unique name for the event. GUI trigger registration will check if "udg_".."ThisEventName" exists, so do not prefix it with udg_.
        ---@return table
        function Event.create(name)
            local event = allocate(name)
           
            ---Register a function to the event.
            ---@param userFunc      function
            ---@param priority?     number      defaults to 0.
            ---@param userTrig?     trigger     only exists if was called from TriggerRegisterVariableEvent, and only useful if this function is hooked.
            ---@param limitOp?      limitop     same as the above.
            ---@return table
            function event.register(userFunc, priority, userTrig, limitOp)
                return addFunc(name, userFunc, priority)
            end

            ---Calls userFunc when the event is run with the specified index.
            ---@param userFunc      function
            ---@param onValue       any         Defaults to currentEvent.data. This is the value that needs to match when the event runs.
            ---@param runOnce?      boolean     Defaults true. If true, will remove itself after being called the first time.
            ---@param priority?     number      defaults to 0
            ---@return table
            function event.await(userFunc, onValue, runOnce, priority)
                onValue = onValue or currentEvent.data
                runOnce = runOnce~=false
                return addFunc(name,
                    function(...)
                        userFunc(...)
                        if runOnce then
                            Event.current.funcData.remove()
                            if event.promise[onValue] == DoNothing then--no further events exist on this, so Hook has defaulted back to DoNothing
                                event.promise[onValue] = nil
                            end
                        end
                    end,
                    priority, onValue
                )
            end
            return event --return the event object. Not needed; the user can just access it via Event.MyEventName
        end
    end

    local createHook
    do
        local realID --Needed for GUI support to correctly detect Set WaitForEvent = SomeEvent.
        realID = {
            n = 0,
            name = {},
            create = function(name)
                realID.n = realID.n + 1
                realID.name[realID.n] = name
                return realID.n
            end
        }

        local function testGlobal(udgName) return globals[udgName] end
        function createHook(name)
            local udgName = "udg_"..name ---@type string|false
            local isGlobal = pcall(testGlobal, udgName)
            local destroy

            udgName = (isGlobal or _G[udgName]) and udgName
            if udgName then --only proceed with this block if this is a GUI-compatible string.
                if isGlobal then
                    globals[udgName] = realID.create(name) --WC3 will complain if this is assigned to a non-numerical value, hence have to generate one.
                else
                    _G[udgName] = name --do this as a failsafe in case the variable exists but didn't get declared in a GUI Variable Event.
                end
                destroy = select(2,
                    hook("TriggerRegisterVariableEvent", --PreciseWait is needed if triggers use WaitForEvent/SleepEvent.
                        function(userTrig, userStr, userOp, priority)
                            if udgName == userStr then
                                Event[name].register(
                                    wrapTrigger and wrapTrigger(userTrig) or
                                    function()
                                        if IsTriggerEnabled(userTrig) and TriggerEvaluate(userTrig) then
                                            TriggerExecute(userTrig)
                                        end
                                    end,
                                    priority, false, userTrig, userOp
                                )
                            else
                                return TriggerRegisterVariableEvent.actual(userTrig, userStr, userOp, priority)
                            end
                        end
                    )
                )
            end
            return function()
                if destroy then destroy() end
                Event[name] = nil
            end
        end

        if remap then
            if sleep then
                remap("udg_WaitForEvent", nil,
                    function(whichEvent)
                        if type(whichEvent) == "number" then
                            whichEvent = realID.name[whichEvent] --this is a real value (globals.udg_eventName) rather than simply _G.eventName (which stores the string).
                        end
                        assert(whichEvent)
                        local co = coroutine.running()
                        Event[whichEvent].await(function() coroutine.resume(co) end)
                        coroutine.yield()
                    end
                )
                remap("udg_SleepEvent", nil,
                    function(duration) --Yields the coroutine while preserving the event index for the user.
                        local funcData, data = currentEvent.funcData, currentEvent.data
                        PolledWait(duration)
                        currentEvent.funcData, currentEvent.data = funcData, data
                    end
                )
            end
            remap("udg_EventSuccess",
                function() return currentEvent.success end,
                function(value) currentEvent.success = value end
            )
            remap("udg_EventOverride",  nil, Event.stop)
            remap("udg_EventIndex",          function() return currentEvent.data end)
            remap("udg_EventRecursion", nil, function(maxDepth) currentEvent.funcData.maxDepth = maxDepth end)
        end
    end
   
    local createExecutor
    do
        local freeze
        freeze = { --this enables the same recursion mitigation as what was introduced in Damage Engine 5
            list = {},
            apply = function(funcData)
                funcData.active = false
                table.insert(freeze.list, funcData)
            end,
            release = function()
                if freeze.list[1] then
                    for _,funcData in ipairs(freeze.list) do
                        funcData.active = true
                    end
                    freeze.list = {}
                end
            end
        }
        function createExecutor(next, promise)
            local function runEvent(promiseID, success, eventID, ...)
                continue = true
                currentEvent.data = eventID
                currentEvent.success = success
                if promise and promiseID then
                    if promise[promiseID] then
                        promise[promiseID](eventID, ...)   --promises are run before normal events.
                    end
                    if promiseID~=eventID and promise[eventID] then --avoid calling duplicate promises.
                        promise[eventID](eventID, ...)
                    end
                end
                next(eventID, ...)
            end

            local runQueue
            return function(...)
                local funcData = currentEvent.funcData
                if funcData then --if another event is already running.
                    runQueue = runQueue or {}
                    table.insert(runQueue, table.pack(...)) --rather than going truly recursive, queue the event to be ran after the already queued event(s).
                    depth[funcData] = depth[funcData] + 1
                    if depth[funcData] > (funcData.maxDepth or 0) then --max recursion has been reached for this function.
                        freeze.apply(funcData)      --Pause it and let it be automatically unpaused at the end of the sequence.
                    end
                else
                    runEvent(...)
                    while runQueue do --This works similarly to the recursion processing introduced in Damage Engine 5.
                        local tempQueue = runQueue
                        runQueue = nil
                        for _,args in ipairs(tempQueue) do
                            runEvent(table.unpack(args, 1, args.n))
                        end
                    end
                    currentEvent.funcData = nil
                    freeze.release()
                end
            end
        end
    end

    ---@param name string
    ---@return table
    function allocate(name)
        assert(type(name)=="string")
        assert(not Event[name])
        local event
        local next = hook(
            name,
            function(eventIndex, ...)
                event.execute(eventIndex, true, eventIndex, ...) --normal Event("MyEvent",...) function call will have the promise ID matched to the event ID, and "success" as true.
            end,
            _PRIORITY, Event, DoNothing, false
        )
        event         = Event[name]
        event.promise = __jarray() --using a jarray allows Lua-Infused GUI to clean up expired promises.
        event.execute = createExecutor(next, event.promise)
        event.destroy = createHook(name)
        return event
    end
end)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
For the API, imo it's best to use the __call metamethod for running an event. This way, you can run an event in the same fashion you normally call functions. This follows the semantics of other programming languages too, for instance Python have a general category of objects called 'callables' (of which a function is its simplest form), which are expected to 'run some tasks when called with function call syntax'. They do this by implementing the __call__ method. I believe the __call metamethod in Lua is also made for similar purpose (If not, maybe we could just streamline it to serve that purpose :)).

Then for the destroy() method, adding
Lua:
setmetatable(self, nil)
would also be good practice. It basically detaches the metatable from the object. If in the create methods we assign the new object its metatable, which in turn gives the object its "object type" and associated methods, then it would be intuitive to have them removed upon destroy() too.

For features suggestion, maybe 'cleanup functions'.
Lua:
local event = Event.create()
event.register(function(...)
    local a = A.create()

    -- some stuff

    -- return an optional cleanup function
    return function()
        a.destroy()
        print("destroying a")
    end
end)
event.register(function(...)
    local b = B.create()

    -- some other stuff

    -- return an optional cleanup function
    return function()
        b.destroy()
        print("destroying b")
    end
end)

local eventCleanup = event(arg1, arg2, arg3) -- call/execute event
-- event execution would then return a function that calls the returned cleanup functions in reverse order
eventCleanup() -- Prints "destroying b" followed by "destroying a"
The cleanup example is kinda useless since it happens immediately, but in reality cleanups would be called in some other parts of the script and usually separated by some time.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Thank you for the feedback! I am really not sure what to make of your suggestions, which is one of the reasons I've taken so long to respond (that, and all of my time has been spent developing vJass2Lua and its supporting structures).

The "__call" metamethod was conceived to imitate other very popular vJass-style event registration methods (OnUnitIndex, onInit, onDestroy) which just say "on (this thing happening) do". The execution of that event doesn't have to have such a "clean" API, since I'd rather require a custom event to have more verbosity than that of the syntax of the libraries which will depend on both (thereby achieving a bit of a "higher" syntax).

Does setmetatable(self, nil) actually do anything that the garbage collector won't do on its own? I'm not sure, but I do like this idea of "disabling" its type if not just for flavor alone.

I'd need more insights into what you mean by "cleanup functions". What does returning those functions do? Wouldn't you rather just attach some data to the event-table itself if you wanted to expand on the functionality? I'm just trying to understand why someone would want that.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Something I forgot to mention in my previous comment is that the benefit of having a similar API for calling functions and events is that it would allow you to pass an event into any other code that accepts a function. It would also allow you to register events inside other events without any special-case handling in the implementation of the execution of events. Nested events would be supported by default by this resource and even by future resources that only anticipates simple callback functions.

Does setmetatable(self, nil) actually do anything that the garbage collector won't do on its own? I'm not sure, but I do like this idea of "disabling" its type if not just for flavor alone.
The gc probably does it, I'm not sure too. But the purpose of setmetatable(self, nil) is to make the destroy() method totally disable all 'class-specific' features (class methods as well as ones inherited from parent classes) of the object while the user still has reference to the object, so as to prevent the possible mishandling of already "destroyed" objects.

I'd need more insights into what you mean by "cleanup functions". What does returning those functions do? Wouldn't you rather just attach some data to the event-table itself if you wanted to expand on the functionality? I'm just trying to understand why someone would want that.
It's probably clearer to see it in action. I'm using a UnitIndexer as an example, in conjunction with a separate system that attaches special effects to newly entering units in the map and conversely destroys those special effects when those units are removed from the game.

Without cleanup functions

Lua:
-- UnitIndexer library
do
  UnitIndexer = {
    onIndex = Event.create(),
    onDeindex = Event.create()
  }
  local function onUnitIndex()
    UnitIndexer.onIndex:run(GetTriggerUnit())
  end
  local function onUnitDeindex()
    UnitIndexer.onDeindex:run(GetTriggerUnit())
  end
  OnGameStart(function()
    -- initialize native jass events for capturing index/deindex events
  end)
end

-- some other system
do
  local tb = {}
  UnitIndexer.onIndex(function (u)
    tb[u] = {
      auraSfx = AddSpecialEffect(u, ...),
      weaponSfx = AddSpecialEffect(u, ...)
    }
  end)
  UnitIndexer.onDeindex(function (u)
    local data = tb[u]
    DestroyEffect(data.auraSfx)
    DestroyEffect(data.weaponSfx)
  end)
end


Using cleanup functions

Lua:
-- UnitIndexer library
do
  UnitIndexer = {
    onIndex = Event.create()
  }
  local onDeindex
  local function onUnitIndex()
    onDeindex = UnitIndexer.onIndex:run(GetTriggerUnit())
    -- onDeindex is also an Event, which contains all functions returned by
    -- each function registered to onIndex
  end
  local function onUnitDeindex()
    onDeindex:run() -- no need to provide arguments, any arguments available to
                    -- onIndex will already be available here, see system below for why
  end
  OnGameStart(function()
    -- initialize native jass events for capturing index/deindex events
  end)
end

-- some other system
do
  UnitIndexer.onIndex(function (u)
    -- runs on unit index
    local auraSfx = AddSpecialEffect(u, ...),
    local weaponSfx = AddSpecialEffect(u, ...)
    return function () -- runs on unit deindex
      DestroyEffect(auraSfx)
      DestroyEffect(weaponSfx)
    end
  end)
end



Then I imagine the event execution function would be something like:
Lua:
function Event:run(...)
  local cleanupEvent = Event.create()
  if self.varStr then
    local args = Event.args --need to be able to access args publicly for registered triggers.
    Event.args = table.pack(...)
    for node in self:loop() do
      local cleanup = node.userFunc(...)
      if cleanup then cleanupEvent(cleanup) end
    end
    Event.args = args
  else
    for node in self:loop() do
      local cleanup = node.userFunc(...)
      if cleanup then cleanupEvent(cleanup) end
    end
  end
  -- You could optionally do something to make sure the returned cleanupEvent
  -- is only callable once
  -- cleanupEvent(function() cleanupEvent:clear() end)
  return cleanupEvent
end
 
Last edited:
Level 4
Joined
Jun 26, 2013
Messages
48
Will also @MyPad here:

I've now also seen [vJASS] - Evaluate Code and [vJASS] - [Library/Package] Pseudo-Var Event and [Lua] - [Lua] Event Listener

@ScorpioT1000 I've also seen your [Lua] - Lua eventDispatcher

Could I get some feedback on this one from either of you? Are there useful features missing here that I am overlooking?
My eventDispatcher is as simple as possible with no dependencies (WLPM is optional) based on common practices from MVC frameworks but I think you need some OOP version. Do you want to instantiate multiple dispatchers and define/inherit events as classes? Not many people write in OOP since lua is prototypal so I don't think it will be in demand
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
@AGD, I've implemented your request for a metatable "destroyer" in the below. This uses the latest version of LinkedList which adds the "destroy" method. The "destroy" method simply sets the metatable to a constant metatable {__mode = "k"}, which should lubricate it for garbage collection.

The example you provided doesn't actually work, because the event has no way to know if the conditions of the Deindex event were fulfilled, thereby running the cleanup for every unit - not just the unit registered to that particular event.

The approach you were trying is:

Lua:
do
  UnitIndexer.onIndex(function (u)
    local auraSfx = AddSpecialEffect(u, ...)
    local weaponSfx = AddSpecialEffect(u, ...)
    local event
    event = UnitIndexer.onDeindex(function(isUnit)
      if isUnit == u then
        DestroyEffect(auraSfx)
        DestroyEffect(weaponSfx)
        event:remove()
      end
    end)
  end)
end

Both your example and mine are able to access auraSfx/weaponSfx without table reads because Lua functions are "scopes" (or, in Lua terminology, "containers"). However, such an appraoch is overall FAR less efficient because it has to loop through every function with registered deindexer (o(N)) and checks their conditions each time. A single table lookup to determine a function to call is far more practical for the CPU to handle, as that uses O(1) complexity (look back at SpellEffectEvent, which only runs one function per spell ID).

If you want to take your first example and make it "prettier" in syntax by not needing the individual data to be attached a table, you can just attach the function container to the table and do everything you need that way:

Lua:
do
  local tb = {}
  UnitIndexer.onIndex(function (u)
    auraSfx = AddSpecialEffect(u, ...),
    weaponSfx = AddSpecialEffect(u, ...)
    tb[u] = function()
      DestroyEffect(auraSfx)
      DestroyEffect(weaponSfx)
    end
  end)
  UnitIndexer.onDeindex(function(u)
    tb[u]()
    tb[u] = nil
  end)
end

@ScorpioT1000 I agree that dependencies should be as few as possible (within reason). However, I think that Lua (and its WarCraft 3 implementation) is just lacking very basic behavior that comes standard with other languages. @Bannar has proposed that we eventually pull together a "WarCraft 3 Lua Standard Library". I don't think we have enough to go on just yet, but it's a sound idea and worth considering in the long-term.

The OOP approach is more to do with extensibility in terms of routing "DamageEvent" and "UnitEvent" and "WhateverEvent" to be based off of this kind of resource. Lua Damage Engine isn't using this (yet) because I had designed everything from the ground up to make it happen, but I ended up building this for the sake of Lua Unit Event due to me noticing that I was re-writing the same core code once again.

The main reason to choose this event library over any other is simply due to the embedded GUI utility, which enables Lua events and GUI events to exist in perfect harmony, without disrupting each other's performance as it does with vJass Damage Engine/UnitEvent/etc.
 
Last edited:
Taking a look at this now, I noticed that there are some features that are not found here that I've implemented in [Lua] - [Lua] Event Listener, which are:
  • Enabling and disabling of callback functions (just like triggers).
  • Prevention of infinite recursion through specifying the callback depth for each instance.
  • {Add-on} Direct access to callback functions.
While infinite recursion prevention might not too problematic to implement here, there's little to no incentive to realize it for those who already know how to deal with infinite recursions. Adding the feature to enable and disable certain callback functions in an Event object would be nice, and allows the users to manually prevent instances of infinite recursion occurring (limited only by the stack).
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Time to share a preview of what I've got planned:

Lua:
OnLibraryInit({         --requires https://www.hiveworkshop.com/threads/global-initialization.317099/
    "AddHook"           --requires https://www.hiveworkshop.com/threads/hook.339153/
    --,"GlobalRemap"    --optional https://www.hiveworkshop.com/threads/global-variable-remapper.339308
}, function()
--[[
CreateEvent v0.1
CreateEvent is built for GUI support, event linking via coroutines, simple events
(e.g. Heal Event) or complex event systems such as Spell Event and Damage Engine.
Due to optional parameters/return values, the API supports a very simple or very
complex environment, depending on how you choose to use it. It is also easily
extensible, so it can be used to support API for other event libraries.
]]
local events={}
local rootEventList={}
local globalSuspendFunc, globalSuspendActivated, createEventIndex, recycleEventIndex
local weakTable={__mode="k"}
---@param eventStr?             string                          For GUI trigger registration
---@param limitOp?              limitop                         For GUI trigger registration
---@param maxEventDepth?        integer                         defaults to 1 if nil. Only in very few cases should this be changed.
---@param multiEventSequence    integer                         If 0, will be first in a chain of events. If an existing event, will be added to that list.
---@param funcSanitizer?        fun(userFunc:function):function takes the user's function and returns whatever you want (e.g. a wrapper func, or nil to prevent the registration).
---@return fun(userFunc:function, priority?:number, manualControl?:boolean):nextEvent:function,removeEvent:fun(pause:boolean),suspendFunc:fun(until_which_event:integer)|nil registerEvent
---@return fun(...)     runEvent        call this with any number of arguments to run all functions registered to this event.  
---@return function     removeEvent     call this to remove the event completely.
---@return integer      eventIndex      useful in multi-event-sequences to identify the previous event in a chain.
function CreateEvent(eventStr, limitOp, multiEventSequence, maxEventDepth, funcSanitizer)
    local thisEvent, registerEvent, running, removeEventHook, removeTRVE, unpauseList, userFuncList
    thisEvent=createEventIndex()
    events[thisEvent]=DoNothing
    if multiEventSequence then
        if multiEventSequence==0 then
            userFuncList=setmetatable({}, weakTable)
            rootEventList[thisEvent]=userFuncList
        else
            userFuncList=rootEventList[multiEventSequence]
        end
    end
    registerEvent=function(userFunc, priority, disablePausing, manualControl)
        local thisList, nextEvent, isPaused, removeUserHook, removeOrPauseFunc, userFuncBaseCaller, userFuncHandler, pauseFuncHandler, suspendFunc
        if multiEventSequence then
            --all data is indexed by the user's function. So, for linked events, the user should pass the
            --same function to each of them, to ensure that they can be connected via one harmonious coroutine:
            thisList=userFuncList[userFunc]
            if not thisList then
                thisList=setmetatable({}, weakTable)
                userFuncList[userFunc]=thisList
                --It is imperitive that the eventIndex is a unique value for the event, in order to ensure that
                --coroutines can persist even with overlapping events that call the same function. A good example
                --is a Unit Index Event which runs when a unit is created, and once more when it is removed. The
                --Event Index can be a unit, and the "on index" coroutine can simply yield until the "on deindex"
                --runs for that exact unit. Another example is simply using a dynamic table for the eventIndex,
                --which is what systems like Damage Engine use.
                local lastEventIndex
                userFuncBaseCaller=function(eventIndex, ...)
                    --transform calling the user's function into a coroutine
                    lastEventIndex=eventIndex
                    local co=coroutine.create(userFunc)
                    coroutine.resume(co, eventIndex, ...)
                    if not manualControl and coroutine.status(co)=="suspended" then
                        nextEvent(eventIndex, ...)
                        return true
                    end
                end
                suspendFunc=function(suspendUntil)
                    --this function will be returned via the registration call.
                    local co=coroutine.running()
                    thisList[lastEventIndex]={co=co, waitFor=suspendUntil}
                    coroutine.yield(not manualControl) --if the coroutine yields, alert the "resume" function accordingly.
                end
                thisList.suspendFunc=suspendFunc
            else
                thisList=userFuncList[userFunc]
                userFuncBaseCaller=function(eventIndex, ...)
                    --completely change the user's function to load and resume a coroutine.
                    local thread=thisList[eventIndex]
                    if thread and thread.waitFor==thisEvent then
                        local co=thread.co
                        coroutine.resume(co)
                        if not manualControl and coroutine.status(co)=="suspended" then
                            nextEvent(eventIndex, ...)
                            return true
                        end
                    end
                end
                suspendFunc=thisList.suspendFunc
            end
        else
            userFuncBaseCaller=userFunc
        end
        if disablePausing then
            pauseFuncHandler=userFuncBaseCaller
        else
            --wrap the user's function
            pauseFuncHandler=function(...)
                if not isPaused then return userFuncBaseCaller(...) end
            end
        end
        if manualControl then
            userFuncHandler=pauseFuncHandler
        else
            --wrap the user's function again
            userFuncHandler=function(...)
                if not pauseFuncHandler(...) or not multiEventSequence then
                    nextEvent(...)
                end
            end
        end
        if funcSanitizer then
            --in case the user wants anything fancier than what's already given, they can add
            --some kind of conditional or data attachment/complete denial of the function.
            userFuncHandler=funcSanitizer(userFuncHandler)
            if userFuncHandler==nil then return end
        end
        --Now that the user's function has been processed, hook it to the event:
        nextEvent,removeUserHook=AddHook(thisEvent, userFuncHandler, priority, events)
        --useful only if the entire event will ultimately be removed:
        removeEventHook=removeEventHook or removeUserHook
        --this incorporates the pause/unpause mechanism of Damage Engine 5, but with more emphasis on the user being in control:
        removeOrPauseFunc=function(pause, autoUnpause)
            if pause==nil then
                removeUserHook()
            else
                isPaused=pause
                if autoUnpause then
                    unpauseList=unpauseList or {}
                    table.insert(unpauseList, removeOrPauseFunc)
                end
            end
        end
        --return the user's 3 functions. suspendFunc will be nil unless this is part of a multiEventSequence.
        return nextEvent, removeOrPauseFunc, suspendFunc
    end
    if eventStr then
        local oldTRVE,cachedTrigFuncs
        if multiEventSequence and not globalSuspendActivated then
            --Apply the below remapping hook only if there is a need for it.
            globalSuspendActivated=true
            GlobalRemap("udg_WaitForEvent", nil, function(val) globalSuspendFunc(val) end)
        end
        --add a hook for seamless GUI trigger compatibility
        oldTRVE,removeTRVE=AddHook("TriggerRegisterVariableEvent",
        function(userTrig, userStr, userOp, userVal)
            if eventStr==userStr and limitOp==nil or userOp==limitOp then
                local cachedTrigFunc, suspendFunc
                if cachedTrigFuncs then
                    cachedTrigFunc=cachedTrigFuncs[userTrig]
                else
                    cachedTrigFuncs=setmetatable({},weakTable)
                end
                if not cachedTrigFunc then
                    cachedTrigFunc=function()
                        if IsTriggerEnabled(userTrig) and TriggerEvaluate(userTrig) then
                            globalSuspendFunc=suspendFunc
                            TriggerExecute(userTrig)
                        end
                    end
                    cachedTrigFuncs[userTrig]=cachedTrigFunc
                    _,_,suspendFunc=registerEvent(cachedTrigFunc, userVal)
                else
                    registerEvent(cachedTrigFunc, userVal)
                end
            else
                return oldTRVE(userTrig, userStr, userOp, userVal)
            end
        end)
    end
    return registerEvent,
    --This function runs the event.
    function(...)
        if running then
            --rather than going truly recursive, queue the event to be ran after the first event, and wrap up any events queued before this.
            if running==true then running={} end
            local args=table.pack(...)
            table.insert(running, function() events[thisEvent](table.unpack(args, 1, args.n)) end)
        else
            running=true
            events[thisEvent](...)
            local depth=0
            while running~=true and depth<maxEventDepth do
                --This is, at its core, the same recursion processing introduced in Damage Engine 5.
                local runner=running; running=true
                for func in ipairs(runner) do func() end
                depth=depth+1
            end
            if unpauseList then
                --unpause users' functions that were set to be automatically unpaused.
                for func in ipairs(unpauseList) do func(false) end
                unpauseList=nil
            end
            --un-comment the below if you want debugging. Mainly useful if you use a depth greater than 1.
            --if depth>=maxEventDepth then
            --    print("Infinite Recursion detected on event: "..(eventStr or thisEvent))
            --end
            running=nil
        end
    end,
    --This function destroys the event.
    function()
        if thisEvent then
            if removeEventHook and events[thisEvent]~=DoNothing then removeEventHook(true) end
            if removeTRVE then removeTRVE() end
            recycleEventIndex(thisEvent)
            thisEvent=nil
        end
    end,
    thisEvent --the event index. Useful for chaining events in a sequence.
end
do
    local eventN=0
    local eventR={}
    --works indentically to vJass struct index handling
    ---@return integer
    createEventIndex=function()
        if #eventR>0 then
            return table.remove(eventR, #eventR)
        else
            eventN=eventN+1
            return eventN
        end
    end
    ---@param index integer
    recycleEventIndex=function(index)
        table.insert(eventR, index)
        events[index]=nil
    end
end
end)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Version 0.2 takes the direction of getting rid of the limitop measurement, and if an event string is provided as the first parameter then it itself will be used as the event ID (rather than a pointless integer).

Lua:
OnLibraryInit({         --requires https://www.hiveworkshop.com/threads/global-initialization.317099/
    "AddHook"           --requires https://www.hiveworkshop.com/threads/hook.339153/
    --,"GlobalRemap"    --optional https://www.hiveworkshop.com/threads/global-variable-remapper.339308
}, function()
--[[
CreateEvent v0.2

CreateEvent is built for GUI support, event linking via coroutines, simple events
(e.g. Heal Event) or complex event systems such as Spell Event and Damage Engine.

Due to optional parameters/return values, the API supports a very simple or very
complex environment, depending on how you choose to use it. It is also easily
extensible, so it can be used to support API for other event libraries.
]]
local events={}
local rootEventList={}
local globalSuspendFunc, globalSuspendActivated, globalEventIndex, createEventIndex, recycleEventIndex
local weakTable={__mode="k"}

---@param eventStr?             string                          Reserved only for GUI trigger registration
---@param multiEventSequence    integer|string                  If 0, will be first in a chain of events. If an existing event, will be added to that list.
---@param maxEventDepth?        integer                         defaults to 1 if nil. Only in very few cases should this be changed.
---@param funcSanitizer?        fun(userFunc:function):function takes the user's function and returns whatever you want (e.g. a wrapper func, or nil to prevent the registration).
---@return fun(userFunc:function, priority?:number, manualControl?:boolean):nextEvent:function,removeEvent:fun(pause:boolean),suspendFunc:fun(until_which_event:integer)|nil registerEvent
---@return fun(...)         runEvent        call this with any number of arguments to run all functions registered to this event. 
---@return function         removeEvent     call this to remove the event completely.
---@return integer|string   eventIndex      useful in multi-event-sequences to identify the previous event in a chain.
function CreateEvent(eventStr, multiEventSequence, maxEventDepth, funcSanitizer)
    local thisEvent, registerEvent, running, removeEventHook, removeTRVE, unpauseList, userFuncList
    thisEvent=eventStr or createEventIndex()
    events[thisEvent]=DoNothing
    if multiEventSequence then
        if multiEventSequence==0 then
            userFuncList=setmetatable({}, weakTable)
            rootEventList[thisEvent]=userFuncList
        else
            userFuncList=rootEventList[multiEventSequence]
        end
    end
    registerEvent=function(userFunc, priority, disablePausing, manualControl)
        local thisList, nextEvent, isPaused, removeUserHook, removeOrPauseFunc, userFuncBaseCaller, userFuncHandler, pauseFuncHandler, suspendFunc
        if multiEventSequence then
            --all data is indexed by the user's function. So, for linked events, the user should pass the
            --same function to each of them, to ensure that they can be connected via one harmonious coroutine:
            thisList=userFuncList[userFunc]
            if not thisList then
                thisList=setmetatable({}, weakTable)
                userFuncList[userFunc]=thisList

                --It is imperitive that the eventIndex is a unique value for the event, in order to ensure that
                --coroutines can persist even with overlapping events that call the same function. A good example
                --is a Unit Index Event which runs when a unit is created, and once more when it is removed. The
                --Event Index can be a unit, and the "on index" coroutine can simply yield until the "on deindex"
                --runs for that exact unit. Another example is simply using a dynamic table for the eventIndex,
                --which is what systems like Damage Engine use.
                local lastEventIndex
                userFuncBaseCaller=function(eventIndex, ...)
                    --transform calling the user's function into a coroutine
                    lastEventIndex=eventIndex
                    local co=coroutine.create(userFunc)
                    coroutine.resume(co, eventIndex, ...)
                    if not manualControl and coroutine.status(co)=="suspended" then
                        nextEvent(eventIndex, ...)
                        return true
                    end
                end
                suspendFunc=function(suspendUntil)
                    --this function will be returned via the registration call.
                    local co=coroutine.running()
                    thisList[lastEventIndex]={co=co, waitFor=suspendUntil}

                    coroutine.yield(not manualControl) --if the coroutine yields, alert the "resume" function accordingly.
                end
                thisList.suspendFunc=suspendFunc
            else
                thisList=userFuncList[userFunc]
                userFuncBaseCaller=function(eventIndex, ...)
                    --completely change the user's function to load and resume a coroutine.
                    local thread=thisList[eventIndex]
                    if thread and thread.waitFor==thisEvent then
                        local co=thread.co
                        coroutine.resume(co)
                        if not manualControl and coroutine.status(co)=="suspended" then
                            nextEvent(eventIndex, ...)
                            return true
                        end
                    end
                end
                suspendFunc=thisList.suspendFunc
            end
        else
            userFuncBaseCaller=userFunc
        end
        if disablePausing then
            pauseFuncHandler=userFuncBaseCaller
        else
            --wrap the user's function
            pauseFuncHandler=function(...)
                if not isPaused then return userFuncBaseCaller(...) end
            end
        end
        if manualControl then
            userFuncHandler=pauseFuncHandler
        else
            --wrap the user's function again
            userFuncHandler=function(...)
                if not pauseFuncHandler(...) or not multiEventSequence then
                    nextEvent(...)
                end
            end
        end
        if funcSanitizer then
            --in case the user wants anything fancier than what's already given, they can add
            --some kind of conditional or data attachment/complete denial of the function.
            userFuncHandler=funcSanitizer(userFuncHandler)
            if userFuncHandler==nil then return end
        end
        --Now that the user's function has been processed, hook it to the event:
        nextEvent,removeUserHook=AddHook(thisEvent, userFuncHandler, priority, events)

        --useful only if the entire event will ultimately be removed:
        removeEventHook=removeEventHook or removeUserHook

        --this incorporates the pause/unpause mechanism of Damage Engine 5, but with more emphasis on the user being in control:
        removeOrPauseFunc=function(pause, autoUnpause)
            if pause==nil then
                removeUserHook()
            else
                isPaused=pause
                if autoUnpause then
                    unpauseList=unpauseList or {}
                    table.insert(unpauseList, removeOrPauseFunc)
                end
            end
        end
        --return the user's 3 functions. suspendFunc will be nil unless this is part of a multiEventSequence.
        return nextEvent, removeOrPauseFunc, suspendFunc
    end
    if eventStr then
        local oldTRVE,cachedTrigFuncs
        if multiEventSequence and not globalSuspendActivated then
            --Apply the below remapping hook only if there is a need for it.
            globalSuspendActivated=true
            GlobalRemap("udg_WaitForEvent", nil, function(val) globalSuspendFunc(val) end)
            GlobalRemap("udg_EventIndex", function() return globalEventIndex end)
        end
        --add a hook for seamless GUI trigger compatibility
        oldTRVE,removeTRVE=AddHook("TriggerRegisterVariableEvent",
        function(userTrig, userStr, userOp, userVal)
            if eventStr==userStr then
                local cachedTrigFunc, suspendFunc
                if cachedTrigFuncs then
                    cachedTrigFunc=cachedTrigFuncs[userTrig]
                else
                    cachedTrigFuncs=setmetatable({},weakTable)
                end
                if not cachedTrigFunc then
                    cachedTrigFunc=function(eventIndex)
                        if IsTriggerEnabled(userTrig) and TriggerEvaluate(userTrig) then
                            globalSuspendFunc=suspendFunc
                            globalEventIndex=eventIndex
                            TriggerExecute(userTrig)
                        end
                    end
                    cachedTrigFuncs[userTrig]=cachedTrigFunc
                    _,_,suspendFunc=registerEvent(cachedTrigFunc, userVal)
                else
                    registerEvent(cachedTrigFunc, userVal)
                end
            else
                return oldTRVE(userTrig, userStr, userOp, userVal)
            end
        end)
    end
    return registerEvent,
    --This function runs the event.
    function(...)
        if running then
            --rather than going truly recursive, queue the event to be ran after the first event, and wrap up any events queued before this.
            if running==true then running={} end
            local args=table.pack(...)
            table.insert(running, function() events[thisEvent](table.unpack(args, 1, args.n)) end)
        else
            running=true
            events[thisEvent](...)
            local depth=0
            while running~=true and depth<maxEventDepth do
                --This is, at its core, the same recursion processing introduced in Damage Engine 5.
                local runner=running; running=true
                for func in ipairs(runner) do func() end
                depth=depth+1
            end
            if unpauseList then
                --unpause users' functions that were set to be automatically unpaused.
                for func in ipairs(unpauseList) do func(false) end
                unpauseList=nil
            end
            --un-comment the below if you want debugging. Mainly useful if you use a depth greater than 1.
            --if depth>=maxEventDepth then
            --    print("Infinite Recursion detected on event: "..(eventStr or thisEvent))
            --end
            running=nil
        end
    end,
    --This function destroys the event.
    function()
        if thisEvent then
            if removeEventHook and events[thisEvent]~=DoNothing then removeEventHook(true) end
            if removeTRVE then removeTRVE() end

            if not eventStr then
                recycleEventIndex(thisEvent)
            end
            thisEvent=nil
        end
    end,
    thisEvent --the event index. Useful for chaining events in a sequence that didn't get tagged to a GUI variable.
end
do
    local eventN=0
    local eventR={}
    --works indentically to vJass struct index handling
    ---@return integer
    createEventIndex=function()
        if #eventR>0 then
            return table.remove(eventR, #eventR)
        else
            eventN=eventN+1
            return eventN
        end
    end
    ---@param index integer
    recycleEventIndex=function(index)
        table.insert(eventR, index)
        events[index]=nil
    end
end
end)

Tests:

Strictly Lua:

Lua:
local register,run,remove,id=CreateEvent(nil, 0)
local register2,run2,remove2,id2=CreateEvent(nil, id)

local next1,next2,pause1,pause2,func1,func2
func1=function()
    print "first function"
    pause1(id2)
    print "first function pt 2"
end
func2=function()
    print "second function"
    pause2(id2)
    print "second function pt 2"
end
next1,_,pause1=register(func1)
next2,_,pause2=register(func2)
register2(func1)
register2(func2)

run("main seq")
print("====first event ran====")

run2("main seq")

print("====both events ran====")

run("second seq")

--Prints:
--[[
second function
first function
====first event ran====
second function pt 2
first function pt 2
====both events ran====
second function
first function
]]

GUI (but Lua is required to initially create the events):

Lua:
local event1={CreateEvent("udg_BasicEvent1", 0)}
local event2={CreateEvent("udg_BasicEvent2", "udg_BasicEvent1")} --assign BasicEvent2 as the next event in the sequence starting with BasicEvent1
OnGameStart(function()
    local eventId1={}
    local eventId2={}
    local eventId3={}
    event1[2](eventId1)
    event1[2](eventId2)
    event1[2](eventId3)
 
    event2[2](eventId2)
    event2[2](eventId1)
    event2[2](eventId3)
end)

  • Basic Event Test
    • Events
      • Game - Value of BasicEvent1 becomes Equal to 1.00
      • Game - Value of BasicEvent2 becomes Equal to 1.00
    • Conditions
    • Actions
      • Set MyInteger = (MyInteger + 1)
      • Set IntegerArray[EventIndex] = MyInteger
      • Set WaitForEvent = BasicEvent2
      • Game - Display text to (All players) Integer(IntegerArray[EventIndex])
      • -------- Should display the following: --------
      • -------- 2 --------
      • -------- 1 --------
      • -------- 3 --------
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
CreateEvent is finally here! I've attached a map showing this thing in action, and I am quite pleased with what this means for the future of GUI Events. It is going to serve as the backbone for Lua Spell System, Lua Unit Event, Lua Heal Event, and...

Lua Attack Engine (which will combine AttackIndexer and Damage Engine, so you can use the attack event, wait until missile launch, wait until missile hit, wait until armor has been processed, all in ONE trigger, thanks to the coroutines enabled by CreateEvent's linked events.
 

Attachments

  • CreateEvent Test.zip
    47.8 KB · Views: 7

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've created a couple of examples below which connect the dots between what CreateEvent is doing, and what a couple of other event libraries are doing.

This bridges the API of @ScorpioT1000's eventDispatcher:

Lua:
-- Warcraft 3 eventDispatcher module by ScorpioT1000 / 2020, edited by Bribe as proof of concept for showing how CreateEvent works.
OnGlobalInit(function()

  Require "CreateEvent"

  local events={}
  local funcs={}
 
  local sanitizer = function(userFunc)--shows how to use the funcSanitizer mechanism of CreateEvent to make it compatible with eventDispatcher's API.
    return function(event)            --it's obviously faster to just use the built-in functionality that CreateEvent provides than this.
      userFunc(event)
      if not event.stopPropagation then
        funcs[userFunc][1](event)--call the next registered function
      end
    end
  end
 
  eventDispatcher = {
    ---@param eventName string
    ---@param callback function
    on = function(eventName, callback, priority) --added priority as a 3rd parameter.
      if(events[eventName] == nil) then
        --the below parameters:
        --1) skip the "string" behavior of CreateEvent, as that's only used to enable GUI support (which eventDispatcher doesn't use)
        --2) skip the event linking parameter, because eventDispatcher doesn't support that.
        --3) skip the recursion depth parameter. eventDispatcher doesn't have any recursion protection, but since CreateEvent can handle that passively,
        --   just keep it as a perk.
        --4) specify that we want to "sanitize" the user's function with our custom function defined above. In this case, we're just adding a wrapper.
        events[eventName] = {CreateEvent(nil, nil, nil, sanitizer)} --cache the return values of CreateEvent
      end
      --The below parameters:
      --1) pass the user's function (notice though that we didn't need to use the sanitizer - we could've wrapped it here)
      --2) pass the priority (if the user adopted it)
      --3) disables pausing (eventDispatcher doesn't use it)
      --4) pass "true" to enable manual control over whether to call the "next" registered function or not.
      funcs[callback]={events[eventName][1](callback, priority, true, true)} --the first return value from CreateEvent is the registration function.
    end,
 
    ---@param eventName string
    ---@param specialCallback function
    off = function(eventName, specialCallback)
      if specialCallback and funcs[specialCallback] then
        funcs[specialCallback][2]() --the 2nd return value from CreateEvent's registration function is the remove function.
     
      elseif events[eventName] then
        events[eventName][3]() --the 3rd return value of CreateEvent removes the event.
        events[eventName] = nil
      end
    end,
 
    ---@param eventName string
    ---@param data
    dispatch = function(eventName, data)
      if events[eventName] then
        events[eventName][2]({ --the 2nd return value of CreateEvent runs the event.
          name = eventName,
          data = data,
          stopPropagation = false
        })
      end
    end,
 
    -- Removes all events
    clear = function()
      for _, event in pairs(events) do
        event[3]() --remove each event
      end
      events,funcs = {},{}
    end
  }
end)

This one bridges the API of @MyPad's EventListener:

Lua:
OnGlobalInit(function()
    Require "CreateEvent"
    
    local funcs={}
    --EventListener's API has been optimized. Recursion in CreateEvent uses DamageEngine 5's approach, allowing a safer experience.
    EventListener={
        ---@param obj? table
        ---@return table event_object
        create=function(obj)
            obj=obj or {}
            local register, run, remove = CreateEvent()
            setmetatable(obj, {
                register=function(_, func, priority) --added parameter to allow a priority to be given to control the order of events.
                    _,funcs[func]=register(func, priority)
                end,
                run=run,         --this inherits the native "run" function returned by CreateEvent. Since the parameters are pretty much the same, it's safe.
                destroy=remove,  --this inherits the native "remove" function returned by CreateEvent. The functionality is pretty much identical.
                enable=function(_, func, unpause, autoUnpause) --added parameter to allow automatic unpausing, as introduced by CreateEvent.
                    if funcs[func] then
                        funcs[func](not unpause, autoUnpause)
                    end
                end,
                unregister=function(_, func)
                    if funcs[func] then
                        funcs[func]()
                        funcs[func]=nil
                    end
                end
            })
            return obj
        end
    }
end)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Another major update - resource has been renamed to Event. The implementation is now much cleaner and harmonizes Lua events with GUI rather nicely:

Create: Event.create "thisEvent"

If "udg_thisEvent" exists, it will allow syntax like "Game - Value of thisEvent becomes Equal to 0.00" to work.

Run: Event.thisEvent(someData)

Register: Event.thisEvent.register(function() print "whoo hoo" end)

Events do not need to have a string declared upon their creation, but as you can see, it makes using those events much easier.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Hello, I'm having a problem, when I pass more than one argument to the run event function the second argument in the callback is passed as nil, and then cause me an error if I use it and then the event never run again.
I see why this is happening, and I'll start thinking about a fix. Sorry for that. The issue is that I have a "special execution" parameter that was added for SpellEvent, but I see it will make more sense to use a different function to handle special callbacks than it would to have them be bundled on the same thing.

The current workaround would be to pass "nil" as the second parameter, but that workaround is just going to be patched out, so don't bother.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Absolutely, spectacularly, mega-awesome update to this resource. Feast your eyes on Event v2.1:
  • Event.create no longer takes a million arguments, and now only just takes one: a name. The name is now required, whereas it was previously optional.
    • Event.MyEventName.await is similar to the JavaScript Promise object. You can instruct a function to wait until the value is run by the event, and it is no longer O(n) like the previous WaitForEvent.
    • Event.MyEventName() runs the event with normal arguments (no more weird stuff like what @HerlySQR had reported)
    • Event.MyEventName.execute is added to handle an extra value and a boolean to indicate "success" of the event. This handles two scenarios:
      • Allows systems like Spell System to have value listeners for a specific ability ID, or Damage Engine to have listeners for the limitop pairings (running separately for attack/spell/code)
      • The boolean allows for a system like Attack Event to communicate to Damage Event that an attack was missed, so the user can run a different block of code for each, depending on the boolean logic. This is unlike JavaScript's Promise, wherein the user has to define multiple functions for "then/catch".
    • Event.MyEventName.await works similarly to WaitForEvent logic, but no longer yields the thread (hence Event no longer needs coroutines for this).
  • WaitForEvent now only exists in GUI. GUI users will need PreciseWait in order to activate this functionality, as it wraps their trigger's actions in coroutines. Lua users should turn to the new Event.MyEventName.registerOnNext.
  • Event.stop() has been added to skip all further callbacks on the event execution instance. This is similar to DamageEventOverride or @ScorpioT1000 's "stopPropogation" functionality.
  • Current event data can now be accessed via Event.current.property. Public properties are:
    • Event.current.funcData --data regarding the currently-running function. This is also returned by the event.register functions.
      • funcData.depth --current depth of its recursion (starting from 0)
      • funcData.maxDepth --max depth of its recursion tolerance (starting from 1)
      • funcData.remove --call this to remove the function
      • funcData.active --set to false to pause the function from being run again until it is unpaused
    • Event.current.data -- the first argument passed to the event to run it)
    • Event.current.success -- true in most cases; only false if the event was specified as failed.
GUI triggers should no longer register multiple events when they want to use the WaitForEvent functionality.

Overall, the API is much cleaner now, and will finally allow me to proceed with my other systems, now that this fundamental logic is out of the way.

The demo scripts in the main post have been updated to show off the new API.

In September, I had released some API to make this work with EventListener and eventDispatcher. That API no longer works. It makes more sense to manually tune a script to use Event than to try to use bridge functionality from either of those resources, as you'll end up with a much shorter code implementation per-script. Here is a guideline for the API translation:

Code:
EventListener:
create([obj:table])       ->  Event.create(name:string)
run(...)                  ->  Event.name(...)
register(func)            ->  Event.name.register(func):funcData
getCurCallback()          ->  Event.current.funcData
unregister(func)          ->  funcData.remove()
getMaxDepth()             ->  funcData.maxDepth
setMaxDepth(value)        ->  funcData.maxDepth = value
getCallbackDepth(func)    ->  funcData.depth
enable(func, flag)        ->  funcData.active = flag
isEnabled(func)           ->  funcData.active

eventDispatcher:
"on" behaves in two ways:
    1) Create the event if it doesn't already exist     -> Event.create(name)
    2) Register a callback function to the event        -> Event.name.register(callbackFunc)
   
"dispatch" behaves in two ways:
    1) Run the event with data attached to it           -> Event.name(data)
    2) data.stopPropogation can prevent further calls   -> Event.stop().
"off" behaves in two ways:
    1) Remove the function from the eventDispatcher     -> funcData.remove()
    2) Remove the event entirely                        -> Event.EventName.destroy()
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've updated the system to version 2.1, this changes some of the new API introduced in version 2, hopefully for the better. The previous post (and the affected part of the demo) have been updated according to the new logic.

myEvent.registerOnNext(function) -> event.await(function) (same behavior, shorter name)
myEvent.registerOnValue(whichValue, runOnce, function)
->
myEvent.await(function, whichValue, isStatic) --the boolean works inversely now. Set to true to keep the event alive after the first call.

I've also removed the "manualNext" option. In its place is the Event.stop() function (GUI users can set EventOverride in order to achieve the same thing). I don't think anyone was using "manualNext", and I also doubt there will be much use for Event.stop. Nevertheless, I have built the Event.stop functionality into the system for those hardcore enough to find a use for it. The main benefits of it over the previous manualNext are:
  1. One less parameter to think about when registering a function
  2. It will skip other event sequences (as now there are potentially two types of promise evaluations that run before the general registrations).
  3. It is far more dangerous and gives too much power to the user to mess things up, so will force me to work extra creatively on a better solution at some point in the future.
The parameters for "myEvent.register" have been massively reduced from previous Event version, as now it takes only a function and a priority. I call that progress, and hopefully will make things way easier to grasp.

The parameters for "myEvent.await" are hopefully not too bad. I wanted to cut back on some API bloat, and doing that required changing the order of the parameters to start with the function.
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
Hi Bribe, great system! I tried to use this system now, but it seems that Hook v6-v7 broke the system, particularly at allocate function, line 277, since newer versions don't do table calls. Do you plan to update this soon?
 
Top