• 🏆 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 2.0 Preview

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Lua:
--[[
    Event v2

    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()    -> 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 (hiding the ugliness) to allow this to work:
    Game - Value of MyEvent becomes Equal to 0.00

    NOTE - the value is the priority used in the event sequence, so higher-registered numbers run first, down to lower numbers.

    --Enhanced event execution:
    Event.MyEvent.execute(promiseID:any, promiseSucceeded:boolean, ...)
        - Run the event with special data attached (e.g. Spell Event uses the ability ID, Damage Engine uses limitops)
        - In most cases, promiseSucceeded should be "true". However (for example) Attack Engine -> Damage Engine data transmission will use "false" to cover "missed" events.

    --Enhanced event registration:
    Event.registerPromise("SpellEffect", 'Amrf', function() print "Medivh's Raven Form was used" end))
        - This is an example of how Spell Event uses the ability ID to distinguish a special callback to this function.
        - Because this uses Hook, any number of special callbacks can be attached to this data (this was not possible previously in Spell System)
        
    Event.waitForEvent
    ==================
    Yields your function until the designated event is called with the same event data as the current event.
]]
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.
    
    Event = {current = {}}

    local allocate, addFunc

    ---@param eventName 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(eventName)
        local event = allocate(eventName)
        
        ---Register a function to the event.
        ---@param userFunc          function
        ---@param priority?         number      defaults to 0
        ---@param useCoroutines?    boolean
        ---@param manualNext?       boolean
        ---@param depthTolerance?   integer     defaults to 1
        ---@param userTrig?         trigger     not used by this function, but could be useful if this function is being hooked
        ---@param userOp?           limitop     not used by this function, but could be useful if this function is being hooked
        ---@return table
        function event.register(userFunc, priority, useCoroutines, manualNext, depthTolerance, userTrig, userOp)
            return addFunc(eventName, userFunc, priority, useCoroutines, manualNext, depthTolerance)
        end

        ---Similar to JavaScript's Promise object
        ---@param whichValue      any
        ---@param userFunc        function
        ---@param runOnce?        boolean
        ---@param priority?       number
        ---@param useCoroutines?  boolean
        ---@param manualNext?     boolean
        ---@param depthTolerance? integer
        ---@return table
        function event.registerPromise(whichValue, userFunc, runOnce, priority, useCoroutines, manualNext, depthTolerance)
            assert(event)
            local promise = event.promise or __jarray()     --using a __jarray allows Lua-Infused GUI to clean up expired promises.
            event.promise = promise
            local funcData
            funcData = addFunc( --add a hook that runs when the specified event is run with the specified index.
                whichValue,
                function(...)
                    userFunc(...)
                    if runOnce then
                        Event.current.funcData.remove()
                        if promise[whichValue] == DoNothing then--no further events exist on this, so Hook has defaulted back to DoNothing
                            promise[whichValue] = nil
                        end
                    end
                end,
                priority, manualNext, depthTolerance, useCoroutines, event.promise
            )
            return funcData
        end
        return event --return the event object. Alternatively, the user can simply access this same value via Event.MyEventName
    end

    local currentEvent = Event.current

    function addFunc(hookIndex, userFunc, priority, manualNext, depth, useCoroutines, hookTable)
        assert(type(userFunc)=="function")
        local funcData = {active = true, depth = 0, maxDepth = depth}
        funcData.next,
        funcData.remove = hook(
            hookIndex,
            function(...)
                if func.active then
                    currentEvent.funcData = funcData
                    if useCoroutines then
                        coroutine.resume(coroutine.create(userFunc), ...)
                    else
                        userFunc(...)
                    end
                    if manualNext then return end
                end
                funcData.next(...)
            end,
            priority, hookTable or Event, DoNothing, false
        )
        return funcData
    end

    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
                    funcData.depth = 0
                end
                freeze.list = {}
            end
        end
    }

    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

    ---@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]

        local udgName = "udg_"..name ---@type string|false
        local isGlobal = pcall(testGlobal, udgName)

        udgName = (_G[udgName] or isGlobal) 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
            event.destroy = select(2,
                hook(
                    "TriggerRegisterVariableEvent", --PreciseWait is needed if triggers use WaitForEvent/SleepEvent.
                    function(userTrig, userStr, userOp, priority)
                        if udgName == userStr then
                            event.register(
                                wrapTrigger and wrapTrigger(userTrig) or
                                function()
                                    if IsTriggerEnabled(userTrig) and TriggerEvaluate(userTrig) then
                                        TriggerExecute(userTrig)
                                    end
                                end,
                                priority, nil, nil, nil, userTrig, userOp
                            )
                        else
                            return TriggerRegisterVariableEvent.actual(userTrig, userStr, userOp, priority)
                        end
                    end
                )
            )
        else
            event.destroy = DoNothing
        end

        local function runEvent(promiseID, success, eventID, ...)
            currentEvent.data=eventID
            local promise = event.promise
            if promise and promiseID then
                currentEvent.success = success
                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
        function event.execute(...)
            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).
                funcData.depth = funcData.depth + 1
                if funcData.depth >= (funcData.maxDepth and math.max(funcData.maxDepth, 1) or 1) 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

        return event
    end
    ---@param whichEvent string|number
    function Event.waitForEvent(whichEvent)
        if type(whichEvent) == "number" then
            whichEvent = realID.name[whichEvent] --this came from a GUI trigger, so extract the name from the real value.
        end
        assert(whichEvent)
        assert(coroutine.isyieldable())
        local co = coroutine.running()
        Event[whichEvent].registerPromise(
            currentEvent.data,
            function() coroutine.resume(co) end,
            true
        )
    end
    if remap then
        if sleep then
            remap("udg_WaitForEvent", nil, Event.waitForEvent)
            remap("udg_SleepEvent", nil, function(duration)
                local id = currentEvent.data
                PolledWait(duration) ---Yields the coroutine while preserving the event index for the user.
                currentEvent.data = id
            end)
            remap("udg_EventSuccess",
                function() return currentEvent.success end,
                function(value) currentEvent.success = value end
            )
        end
        remap("udg_EventIndex", function() return currentEvent.data end)
        remap("udg_EventRecursion",
            function() return currentEvent.funcData.depth end,
            function(depth) currentEvent.funcData.maxDepth = depth end
        )
    end
end)
Last edited:
Top