• 🏆 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] Action [The future of GUI actions]

GUI just got another incredibly powerful tool added to its arsenal - I present to you:

Action.

Action allows you to treat GUI as a modern interface, with modern garbage collection, loop structure, perfect timing, local variables, and function subroutines. You read that correctly, but did you understand it?

First, have a look at the code:

Lua:
--[[
    Action v1.3.0.0 by Bribe

    What it does:
        1) Allows GUI to declare its own functions to be passed to a Lua system.
        2) Automatic variable localization and cleanup for effects.
        3) Automatic variable localization for units, reals, integers, groups and locations.
        4) Recursive local tracking (as long as array indices are not shadowed).

    Why it can benefit:
        1) Allows you to have all of your functions in one GUI trigger
        2) Each trigger and each sub-action within the trigger has access to each others' data.
        3) No need to create the same variables for each new trigger if you use the variables provided here.

    How it works:
        1) In some cases replaces ForForce, allowing you to manipulate the callback function instead.
        2) Attaches data via coroutines, allowing all locals powered by this system to be local to the running instance of the trigger, regardless of how many times it waits.
        3) To destroy (for example) a unit group: Set DestroyGroup = TempGroup1
--]]
OnGlobalInit(function()

    Require "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
    Require "PreciseWait" --https://www.hiveworkshop.com/threads/precise-wait-gui-friendly.316960/

    Action={}
    local systemPrefix="udg_Action_"
    local actions={}
    local cleanTracker
    local topIndex=__jarray()
    local oldForForce,lastIndex
    oldForForce=AddHook("ForForce", function(whichForce, whichFunc)
        if actions[whichForce] then
            local index=lastIndex
            lastIndex=nil
            actions[whichForce](whichFunc, index)
        else
            oldForForce(whichForce, whichFunc)
        end
    end)

    ---@param whichVar      string          --The name of the user-defined global. It will add the udg_ prefix if you don't feel like adding it yourself.
    ---@param onForForce    fun(function)   --Takes the GUI function passed to ForForce and allows you to do whatever you want with it instead.
    ---@param isArray?      boolean         --If true, will pass the array index to the onForForce callback.
    function Action.create(whichVar, onForForce, isArray)
        if whichVar:sub(1,4)~="udg_" then
            whichVar="udg_"..whichVar
        end
        local force = _G[whichVar]
        if isArray then
            if force then
                GlobalRemapArray(whichVar, function(index)
                    lastIndex=index
                    return whichVar
                end)
            end
        else
            if force then
                GlobalRemap(whichVar, function()
                    lastIndex=nil
                    return whichVar
                end)
            end
        end
        actions[whichVar]=onForForce
    end
    local durations=__jarray()
    local getCoroutine=coroutine.running
    Action.create(systemPrefix.."forDuration", function(func)
        local co = getCoroutine()
        if durations[co] then
            while durations[co] > 0 do
                func()
            end
        end
    end)

    GlobalRemap(systemPrefix.."duration", function() return durations[getCoroutine()] end, function(val) durations[getCoroutine()] = val end)
    
    --Look at this: Every time a trigger runs, it can have its own fully-fledged, perfectly MUI hashtable, without having to initialize or destroy it manually.
    local hash = __jarray()
    GlobalRemap(systemPrefix.."hash", function()
        local parent = topIndex[getCoroutine()]
        hash[parent] = hash[parent] or __jarray()
        return hash[parent]
    end)

    ---Nearly the same as Precise PolledWait, with the exception that it tracks the duration.
    ---@param duration real
    function Action.wait(duration)
        local co = getCoroutine()
        if co then
            local t = CreateTimer()
            TimerStart(t, duration, false, function()
                DestroyTimer(t)
                if durations[co] then
                    durations[co] = durations[co] - duration
                end
                coroutine.resume(co)
            end)
            coroutine.yield(co)
        end
    end
    GlobalRemap(systemPrefix.."wait", nil, Action.wait)

    local function cleanup(co)
        if cleanTracker[co] then
            for _,obj in ipairs(cleanTracker[co]) do
                DestroyEffect(obj)
            end
        end
    end

    ---Allows the function to be run as a coroutine a certain number of times (determined by the array index)
    ---Most importantly, the calling function does not wait for these coroutines to complete (just like in GUI when you execute a trigger from another trigger).
    ---@param func function
    ---@param count number
    Action.create(systemPrefix.."loop", function(func, count)
        local top = getCoroutine()
        for _=1,count do
            local co
            co = coroutine.create(function()
                func()
                cleanup(co)
            end)
            topIndex[co]=top
            coroutine.resume(co)
        end
    end, true)
    local oldAction
    oldAction = AddHook("TriggerAddAction", function(trig, func)
        return oldAction(trig, function()
            local co = getCoroutine()
            topIndex[co]=co
            func()
            cleanup(co)
        end)
    end, 2)

    GlobalRemap(systemPrefix.."index", function() return topIndex[getCoroutine()] end)

    for _,name in ipairs {
        "point",
        "group",
        "effect",
        "integer",
        "real",
        "unit"
    } do
        local tracker=__jarray()
        local varName=systemPrefix..name
        if name=="effect" then
            cleanTracker=tracker
        end
        GlobalRemapArray(varName, function(index)
            local co = getCoroutine()
            ::start::
            local result = rawget(tracker[co], index) --check if the value is already assigned at the level of the current coroutine
            if not result and topIndex[co]~=co then
                --If not, hunt around and try to find if any calling coroutines have the index assigned.
                co = topIndex[co]
                goto start
            end
            return result
        end,
        function(index, val)
            local tb = tracker[getCoroutine()]
            if not tb then
                tb={}
                tracker[getCoroutine()]=tb
            elseif name=="effect" and tb[index] then
                DestroyEffect(tb[index])
            end
            tb[index]=val
        end)
    end
end)

Still stuck? I can't say I blame you. The code is not for the faint of heart. It is one of the ugliest hacks I've ever pulled off in Lua, but it allows the most beautiful GUI trigger syntax the WarCraft 3 world has ever seen. The below is a showcase (not a final version) of how Lua Spell Event and Action interact with each other to deliver an extremely cool effect, with one short trigger:

  • Blizzard Enhancer
    • Events
      • Game - Button for ability Blizzard and order Human Spellbreaker - Spell Steal pressed.
      • Game - OnSpellEffect becomes Equal to 0.00
    • Conditions
    • Actions
      • Special Effect - Create a special effect at Spell__TargetPoint using Abilities\Spells\Human\FlameStrike\FlameStrikeTarget.mdl
      • Set VariableSet Action_effect[1] = (Last created special effect)
      • Player Group - Pick every player in Spell__whileChannel and do (Actions)
        • Loop - Actions
          • Player Group - Pick every player in Action_loop[(4 + (2 x Spell__Level))] and do (Actions)
            • Loop - Actions
              • Set VariableSet Spell__wait = 0.25
              • Set VariableSet Action_point[1] = (Spell__TargetPoint offset by (Random real number between 0.00 and (200.00 x Spell__LevelMultiplier)) towards (Random angle) degrees.)
              • Special Effect - Create a special effect at Action_point[1] using Abilities\Spells\Demon\RainOfFire\RainOfFireTarget.mdl
              • Special Effect - Destroy (Last created special effect)
              • Unit - Cause Spell__Caster to damage circular area after 0.00 seconds of radius 20.00 at Action_point[1], dealing 20.00 damage of attack type Spells and damage type Normal
              • Set VariableSet Spell__wait = 0.75
          • Set VariableSet Spell__wait = 1.00

Action allows automatic destruction of unit groups, effects and locations/points, depending on whether the object is being overwritten (thanks to @Jampion for that idea), whether the Action_loop has completed, or at the latest, by the time the trigger has wrapped up.


Here is an example of a heal-over-time system. The rejuvenation effect is automatically destroyed at the end of the trigger, because to Action_effect is assigned to it.
  • Holy Light Enhancer
    • Events
      • Game - Button for ability Holy Light and order Human Spellbreaker - Spell Steal pressed.
      • Game - OnSpellEffect becomes Equal to 0.00
    • Conditions
    • Actions
      • Special Effect - Create a special effect attached to the chest of Spell__Target using Abilities\Spells\NightElf\Rejuvenation\RejuvenationTarget.mdl
      • Set VariableSet Action_effect[1] = (Last created special effect)
      • Set VariableSet Action_duration = (2.00 + (2.00 x Spell__LevelMultiplier))
      • Player Group - Pick every player in Action_forDuration and do (Actions)
        • Loop - Actions
          • Set VariableSet Spell__wait = 1.00
          • Unit - Set life of Spell__Target to (((Life of Spell__Target) + 5.00) + (5.00 x Spell__LevelMultiplier))


Things to understand about how this works:
  • The effect and locations are automatically destroyed/removed at the end.
  • The Player Groups are not actually player groups, but hookable declarations to access a block of GUI code as a function.
  • Action_loop will start a new coroutine "array index" number of times. This allows subroutines to have their own waits that do not require the root trigger to yield while they do their thing.
  • Array indices in Lua start at 1. Any "Action" array value should also have its index starting at 1.

I feel like I'm still just starting to scratch the surface of what Lua can do for GUI. If Blizzard won't provide GUI the powerful tools it needs to take on the future, I will bring the future to GUI through Lua.
 

Attachments

  • Action and Spell Event Glimpse.zip
    84.1 KB · Views: 19
Last edited:
Thinking of adding these to the API, but I'm tossed between adding them here or in GUI Enhancer Collection:

Lua:
    GlobalRemap("udg_RemoveLocation", nil, RemoveLocation) --point in GUI
    GlobalRemap("udg_CreateGroup", CreateGroup) --unit group in GUI
    GlobalRemap("udg_DestroyGroup", nil, DestroyGroup) --unit group in GUI
    GlobalRemap("udg_RemoveRect", nil, RemoveRect) --region in GUI
    GlobalRemap("udg_DestroyForce", nil, DestroyForce) --player group in GUI
    GlobalRemap("udg_CreateTimer", CreateTimer) --countdown timer in GUI
    GlobalRemap("udg_DestroyTimer", nil, DestroyTimer) --countdown timer in GUI
    GlobalRemap("udg_GetLocalPlayer", GetLocalPlayer) --player in GUI
    GlobalRemap("udg_PrintString", nil, print) --string in GUI
    GlobalRemap("udg_CreateTable", function() return {} end) --integer in GUI

All but the last two functions add API that strictly doesn't exist in GUI, but should. You cannot create an empty unit group in GUI, nor can you create a countdown timer. Therefore, this gives GUI the ability to do both, and also gives them control over the removal of other objects that need custom script.

Ideally, no GUI user should have to use custom script, if there is a Lua function that can be remapped to a GUI process to do it for them (which, I have discovered, is possible in 100% of cases, thanks to Action allowing "parameters" and "return values" based on this ForForce hook).

Ultimately, Action is intended to do stuff like this:

  • Do a Knockback
    • Events
    • Conditions
    • Actions
      • Player Group - Pick every player in Knockback2D_execute and do (Actions)
        • Loop - Actions
          • Set VariableSet Knockback2DUnit = (some unit)
          • Set VariableSet Knockback2DAngle = (Random angle)
          • Set VariableSet Knockback2DDistance = 600
          • Set VariableSet Knockback2DTime = 2.00
But, because default variables can also be set, I could also do this, which automatically knocks a unit back at a random angle:

  • Do a Knockback
    • Events
    • Conditions
    • Actions
      • Set VariableSet Knockback2D_simple = (some unit)
Or, probably a better option, because the angle is often very important to get right:

  • Do a Knockback
    • Events
    • Conditions
    • Actions
      • -------- Knock a unit back at a specified angle: --------
      • Set VariableSet Knockback2D_simple[(Integer(some angle))] = (some unit)
      • -------- Or even knock a unit back at an automatic angle based on the source unit: --------
      • Set VariableSet Knockback2D_simple[(Key(some source unit))] = (some unit)
      • -------- Or, if you have a different source location set already: --------
      • Set VariableSet Knockback2D_simple[(Key(some location))] = (some unit)
Which now makes me think that a really good shortcut for GUI to have is AngleBetweenUnits:

Lua:
GlobaRemapArray("udg_AngleBetweenUnits", nil, function(source, target)
    return bj_RAD2DEG * Atan2(GetUnitY(target)-GetUnitY(source),GetUnitX(target)-GetUnitX(source))
end)

This works if replacing GetHandleIdBJ with a function that just returns the object itself (which, in turn, would need hashtables to be overridden, which has already been accomplished here: [Lua] - [Lua] Hashtable2Table)

I'm unsure, however, if the GUI (Key()) functionality inlines into an array. If not, then it would need 2 lines, rather than 1.
 
Last edited:
I'm going to be publishing a completely earth shattering resource soon which automatically destroys all location memory leaks by transforming all natives which use locations to use tables with x,y components instead (since these get garbage collected). There is one single location that has to be used for stuff like GetLocationZ. I did everything for locations aside from the testing on Thursday night, and will be able to continue work later on in the weekend.

My plan is then to do the same with Unit Groups, which can also be overridden.
 
Holy cow Bribe do you ever not deliver? I can't wait to have a look at this, honestly looks like it will eliminate what I hate most about using the GUI
My main goal is to make Lua the standard. The vast majority of maps and spells are still published using archaic JASS at their core.

vJass had stuff like this:

JASS:
globals
    string array strings
endglobals
function onExpire takes nothing returns nothing
    local integer data = ReleaseTimer(GetExpiredTimer())
    call BJDebugMsg(strings[data])
    call data.destroy()
endfunction
function CallDelayed takes string s returns nothing
    local data = someStruct.create()
    set strings[data] = s
    call TimerStart(NewTimerEx(data), 5.00, false, function onExpire)
endfunction

But in Lua:
Lua:
function CallDelayed(s)
    Timed.call(5.00, function() print(s) end)
end
 
You'll revolutionise mapmaking if you keep this up. I hadn't realised Lua can overwrite natives - that's such a powerful opportunity to improve the GUI, I'm glad you're using it.
Happy it's helping. [Lua] - Lua-Infused GUI + Automatic Group, Location, Rect and Force leak prevention ended up taking care of memory leaks in a much more manageable way than this resource originally set out to do, but it is still good to be able to build everything in one trigger here.
 
Top