• 🏆 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] Fast Triggers [GUI-friendly]

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
This exists primarily to improve things such as manual event execution as well as unify GUI actions into Conditions (see further below for how to enable "Wait" calls from your triggers).

How it works: by hooking the natives "TriggerAddAction" and "TriggerAddCondition + Condition", I can extract the function that is being passed to the natives and, subsequently, call that function manually instead of letting the trigger spit it out.

So with GUI triggers with conditions and actions, the conditions will be called manually and the actions will be called manually if the conditions are true.

If you don't want your Lua resource be affected by this, then I recommend using (Filter (or any other kind of boolexpr)) instead of (Condition) when defining conditions for your Lua trigger. I am not sure if anyone is using TriggerActions in Lua, nor why, but if you need multiple TriggerActions attached to the same trigger, then this resource will not work. A better solution for such odd scenarios is to switch your Actions to Conditions, or to use ExecuteFunc if you need waits.

Requires: [Lua] - Hook

Lua:
if Hook then --https://www.hiveworkshop.com/threads/hook.339153

    --Lua Fast Triggers v1.4.1.0
    --Completely overwrites the BJ "ConditionalTriggerExecute" rather than hooking it, due to performance reasons.
   
    local _PRIORITY = -1 --Specify the hook priority for TriggerAddAction and TriggerAddCondition
   
    local cMap = {}
    local aMap = {}
    local lastCondFunc
    local waitFunc
   
    --hook.args = {1:code}
    Hook.add("Condition",
    function(hook)
        lastCondFunc = hook.args[1]
    end)
   
    --hook.args = {1:trigger, 2:boolexpr}
    Hook.add("TriggerAddCondition",
    function(hook)
        if lastCondFunc then
            local trig = hook.args[1]
            local cond = lastCondFunc
            cMap[trig] = cond --map the condition function to the trigger.
            aMap[trig] = aMap[trig] or DoNothing
            lastCondFunc = nil
           
            hook.args[2] = Filter(
            function()
                if cond() then --Call the triggerconditions manually.
                    waitFunc = aMap[trig]
                    waitFunc() --If this was caused by an event, call the trigger actions manually.
                end --always return nil to prevent WC3 from executing any trigger actions.
            end)
        end
    end, _PRIORITY)
   
    --hook.args = {1:trigger, 2:code}
    Hook.add("TriggerAddAction",
    function(hook)
        local act = hook.args[2]
        aMap[hook.args[1]] = act
       
        hook.args[2] =
        function()
            waitFunc = act
            waitFunc() --If this was caused by an event, call the trigger actions manually.
        end
    end, _PRIORITY)
   
    --hook.args = {1:trigger}
    Hook.add("TriggerExecute",
    function(hook)
        waitFunc = aMap[hook.args[1]]
        hook.skip = true
        waitFunc()
    end)
   
    local skipNext
    function EnableWaits()
        if skipNext then
            skipNext = nil
        else
            skipNext = true
            coroutine.resume(coroutine.create(function()
                waitFunc()
            end))
            return true
        end
    end
   
    function ConditionalTriggerExecute(trig)
        local c = cMap[trig]
        if c and not c() then return end
        local a = aMap[trig]
        if a then a() end
    end
   
    function GetTriggerActionFunc(trig)
        return aMap[trig]
    end
   
    function GetTriggerConditionFunc(trig)
        return cMap[trig]
    end
end

To enable your trigger actions to use "Waits" again (ie. PolledWait), use [Lua] Perfect PolledWait (GUI-friendly) and put the following one line of Custom Script at the VERY TOP of your trigger actions list: if EnableWaits() then return end. The system will handle the rest.

  • Want to wait
    • Events
      • -------- Some event
    • Conditions
      • -------- Some conditions
    • Actions
      • Custom script: if EnableWaits() then return end
      • Wait - 0.50 seconds of game time
      • -------- Do further actions here.
This resource overrides the GUI function "ConditionalTriggerExecute" (Run Trigger (checking conditions)) and has been benchmarked as SEVENTEEN TIMES faster than the below. It is also worth noting that, without this system, there is a very noticeable lag when benchmarking in very high numbers, even if the system clock claims to indicate a benchmark time period much less than what was actually impacting the game performance.

Lua:
    -- This is between 16-18 times slower than what is done with the Fast Triggers approach.
    function ConditionalTriggerExecute(trig)
        if IsTriggerEnabled(trig) and TriggerEvaluate(trig) then
            TriggerExecute(trig)
        end
    end
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Will this work seamlessly with your gui spell system?
Spell System is on my to-do list, so definitely you can expect it sometime towards the end of this week or very early next.

I've updated this script today to enable compatibility with the Lua PerfectWait system if needed.

EDIT on 28 July 2019 - due to Damage Engine needing some fixes this got delayed quite a bit. Should expect this now by mid to late August when I am back from vacation and have gotten the higher-priority resources done.

Edit on 27 May 2021 - obviously this was never done. I have no plans to continue Lua development, so I will most likely never make a Lua-based Spell System.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Updated to v1.1

This update uses the new Hook library to ensure safe function overwrites. Now no longer hooks TriggerEvaluate as there is no utility in doing so (it may likely even be less optimal).

Due to the overhead of the “hook” library, the main benefits are where you access “GetTriggerAction/ConditionFunc” via this library and use them in your own resource to optimize those callbacks.

Updated to v1.2

Now overwrites (rather than hooks) the BJ "ConditionalTriggerExecute" for performance reasons. I also plan to use ConditionalTriggerExecute for the update to Lua DamageEngine (once it's ready, which is still going to take a while at this rate).

Updated to v1.3 to be compatible with Hook 3.4.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Is it possible to override TriggerAddCondition() and TriggerAddAction() to limit the triggerconditions and triggeractions to 1 per trigger? Then the single triggercondition/triggeraction of a trigger will be the one in charge of calling the list of functions "registered" to the trigger.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Is it possible to override TriggerAddCondition() and TriggerAddAction() to limit the triggerconditions and triggeractions to 1 per trigger? Then the single triggercondition/triggeraction of a trigger will be the one in charge of calling the list of functions "registered" to the trigger.
I am guessing that this should be able to work, but it is oddly specific. Do you mean one triggeraction/condition per event, or per trigger? The latter case would only be found in a Lua environment, rather than in a typical GUI trigger.

However, currently this actually doesn’t support multiple actions or conditions on the same trigger, since the indexing is hardcoded to include at most one of each. It would be simple enough to lob on a triggeraction/condition if the index is already found, but deallocation could get annoying if the user wants to do something dynamically.

I originally built this just to make triggers run much faster by turning their actions and conditions into regular function calls, which in that sense would be better to map multiple actions/conditions to a single common event.

In all, I envisioned this being implemented as a “drop in” in a map (like Item Cleanup) that just provides an invisible benefit without needing special API. So I am wary of adding too many features into this that might make it turn into something that other systems rely on.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
However, currently this actually doesn’t support multiple actions or conditions on the same trigger, since the indexing is hardcoded to include at most one of each. It would be simple enough to lob on a triggeraction/condition if the index is already found, but deallocation could get annoying if the user wants to do something dynamically.
Would this break Lua code that relies on multiple actions/conditions on the same trigger?

In all, I envisioned this being implemented as a “drop in” in a map (like Item Cleanup) that just provides an invisible benefit without needing special API. So I am wary of adding too many features into this that might make it turn into something that other systems rely on.
Is the special treatment of waits required, because they do not work inside conditions? If so, maybe it's better to combine Actions and Conditions on the Action side to make it easier for the user and make it a true "drop in". I'd assume the main performance benefit comes from starting less threads and the difference between a single Action and a single Condition isn't too big.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Would this break Lua code that relies on multiple actions/conditions on the same trigger?


Is the special treatment of waits required, because they do not work inside conditions? If so, maybe it's better to combine Actions and Conditions on the Action side to make it easier for the user and make it a true "drop in". I'd assume the main performance benefit comes from starting less threads and the difference between a single Action and a single Condition isn't too big.
Thanks, I have updated the description with the following:

If you don't want your Lua resource be affected by this, then I recommend using (Filter (or any other kind of boolexpr)) instead of (Condition) when defining conditions for your Lua trigger. I am not sure if anyone is using TriggerActions in Lua, nor why, but if you need multiple TriggerActions attached to the same trigger, then this resource will not work. A better solution for such odd scenarios is to switch your Actions to Conditions, or to use ExecuteFunc if you need waits.

In terms of performance, I can't recall exactly, but I think Conditions are 2-4 times faster than Actions. And TriggerExecute was about 50-100% faster than ExecuteFunc. Not sure what these look like individually in Lua.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
For compatibility with multiple Actions/Conditions:

An easy improvement could be to use your Global Initialization to only have this resource active while GUI triggers are initialized. That way most other Lua code will be unaffected.

While, I think it should be possible to support more complex and dynamic Actions/Conditions, it could be quite a lot of effort and actually detrimental to performance for code that adds and removes a lot of Actions/Conditions dynamically.
For both actions and conditions you could have a list and a map. The list stores the callback code in the correct order and the map maps from
triggeraction/triggercondition to the list elements so that you can quickly remove elements from the list when TriggerRemoveAction/Condition is called.




I did some benchmarks and Conditions are indeed quite a bit faster.

For triggers caused by events (the time to trigger the events is not included in the measurements):
Empty Trigger: 9.1µs
Condition (with Empty function body): 10.4 µs
Action (with Empty function body): 13.2 µs
If you consider the Empty Trigger as simply running the trigger, you can say a Condition takes an additional 1.3µs and an Action an additional 4.1µs, so around 3 times faster.
For comparison, giving the player 1 gold takes around 4µs, so the difference between Conditions and Actions is the same as a cheap operation.

While individually Conditions are significantly faster than Actions (4.1µs vs 1.3µs), if you consider the overhead from running the trigger (9.1µs) and adding a very basic code (4µs), the relative difference becomes a lot smaller (17.2µs vs 14.4µs). I'd probably prefer the convenience of not having to worry about waits, but it's really up to you what you value more.

For an empty trigger, TriggerExecute takes around 5µs and TriggerEvaluate 0.6µs. I've found no difference between the increased time caused by adding an Action to TriggerExecute or adding a Condition to TriggerEvaluate. However, since TriggerEvaluate has a noticeably lower overhead and it is the function which runs the Conditions, using Conditions is again better.

Lua:
function LuaMain()
    function Benchmark(n, name, f)
        local start = os.clock()
        for i = 1, n do
            f()
        end
        local dur = os.clock() - start
        local gold = GetPlayerState(Player(0), PLAYER_STATE_RESOURCE_GOLD)
        print(dur .. " | " .. name .. " | " .. gold)
    end
    local OnEvent = function()
        --AdjustPlayerStateSimpleBJ(Player(0), PLAYER_STATE_RESOURCE_GOLD, 1)
    end
    local u1 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u2 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u3 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u4 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local t1 = CreateTrigger()
    TriggerRegisterUnitEvent(t1, u1, EVENT_UNIT_DAMAGED)
    TriggerAddAction(t1, OnEvent)
    local t2 = CreateTrigger()
    TriggerRegisterUnitEvent(t2, u2, EVENT_UNIT_DAMAGED)
    TriggerAddCondition(t2, Condition(OnEvent))
    local t3 = CreateTrigger()
    TriggerRegisterUnitEvent(t3, u3, EVENT_UNIT_DAMAGED)
    function DamageTarget(target)
        UnitDamageTarget(u4, target, 0.0001, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
    end
    local COUNT = 100000
    function PerformBenchmark()
        Benchmark(COUNT, "DamageUnit -> Event -> Action", function()
            DamageTarget(u1)
        end)
        Benchmark(COUNT, "DamageUnit -> Event -> Condition", function()
            DamageTarget(u2)
        end)
        Benchmark(COUNT, "DamageUnit -> Event -> Nothing", function()
            DamageTarget(u3)
        end)
        Benchmark(COUNT, "DamageUnit -> No Event", function()
            DamageTarget(u4)
        end)
        Benchmark(COUNT, "TriggerExecute -> Action", function()
            TriggerExecute(t1)
        end)
        Benchmark(COUNT, "TriggerExecute -> Condition", function()
            TriggerExecute(t2)
        end)
        Benchmark(COUNT, "TriggerExecute -> Nothing", function()
            TriggerExecute(t3)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Action", function()
            TriggerEvaluate(t1)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Condition", function()
            TriggerEvaluate(t2)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Nothing", function()
            TriggerEvaluate(t3)
        end)
    end


    PerformBenchmark()
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
For compatibility with multiple Actions/Conditions:

An easy improvement could be to use your Global Initialization to only have this resource active while GUI triggers are initialized. That way most other Lua code will be unaffected.

While, I think it should be possible to support more complex and dynamic Actions/Conditions, it could be quite a lot of effort and actually detrimental to performance for code that adds and removes a lot of Actions/Conditions dynamically.
For both actions and conditions you could have a list and a map. The list stores the callback code in the correct order and the map maps from
triggeraction/triggercondition to the list elements so that you can quickly remove elements from the list when TriggerRemoveAction/Condition is called.




I did some benchmarks and Conditions are indeed quite a bit faster.

For triggers caused by events (the time to trigger the events is not included in the measurements):
Empty Trigger: 9.1µs
Condition (with Empty function body): 10.4 µs
Action (with Empty function body): 13.2 µs
If you consider the Empty Trigger as simply running the trigger, you can say a Condition takes an additional 1.3µs and an Action an additional 4.1µs, so around 3 times faster.
For comparison, giving the player 1 gold takes around 4µs, so the difference between Conditions and Actions is the same as a cheap operation.

While individually Conditions are significantly faster than Actions (4.1µs vs 1.3µs), if you consider the overhead from running the trigger (9.1µs) and adding a very basic code (4µs), the relative difference becomes a lot smaller (17.2µs vs 14.4µs). I'd probably prefer the convenience of not having to worry about waits, but it's really up to you what you value more.

For an empty trigger, TriggerExecute takes around 5µs and TriggerEvaluate 0.6µs. I've found no difference between the increased time caused by adding an Action to TriggerExecute or adding a Condition to TriggerEvaluate. However, since TriggerEvaluate has a noticeably lower overhead and it is the function which runs the Conditions, using Conditions is again better.

Lua:
function LuaMain()
    function Benchmark(n, name, f)
        local start = os.clock()
        for i = 1, n do
            f()
        end
        local dur = os.clock() - start
        local gold = GetPlayerState(Player(0), PLAYER_STATE_RESOURCE_GOLD)
        print(dur .. " | " .. name .. " | " .. gold)
    end
    local OnEvent = function()
        --AdjustPlayerStateSimpleBJ(Player(0), PLAYER_STATE_RESOURCE_GOLD, 1)
    end
    local u1 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u2 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u3 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local u4 = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('hfoo'), 0, 0, 0)
    local t1 = CreateTrigger()
    TriggerRegisterUnitEvent(t1, u1, EVENT_UNIT_DAMAGED)
    TriggerAddAction(t1, OnEvent)
    local t2 = CreateTrigger()
    TriggerRegisterUnitEvent(t2, u2, EVENT_UNIT_DAMAGED)
    TriggerAddCondition(t2, Condition(OnEvent))
    local t3 = CreateTrigger()
    TriggerRegisterUnitEvent(t3, u3, EVENT_UNIT_DAMAGED)
    function DamageTarget(target)
        UnitDamageTarget(u4, target, 0.0001, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
    end
    local COUNT = 100000
    function PerformBenchmark()
        Benchmark(COUNT, "DamageUnit -> Event -> Action", function()
            DamageTarget(u1)
        end)
        Benchmark(COUNT, "DamageUnit -> Event -> Condition", function()
            DamageTarget(u2)
        end)
        Benchmark(COUNT, "DamageUnit -> Event -> Nothing", function()
            DamageTarget(u3)
        end)
        Benchmark(COUNT, "DamageUnit -> No Event", function()
            DamageTarget(u4)
        end)
        Benchmark(COUNT, "TriggerExecute -> Action", function()
            TriggerExecute(t1)
        end)
        Benchmark(COUNT, "TriggerExecute -> Condition", function()
            TriggerExecute(t2)
        end)
        Benchmark(COUNT, "TriggerExecute -> Nothing", function()
            TriggerExecute(t3)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Action", function()
            TriggerEvaluate(t1)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Condition", function()
            TriggerEvaluate(t2)
        end)
        Benchmark(COUNT, "TriggerEvaluate -> Nothing", function()
            TriggerEvaluate(t3)
        end)
    end


    PerformBenchmark()
end
If that is true, then it would seem that Lua has significantly cut down on the performance impact of the dynamic code of conditions and actions. I was thinking very JASS-y when I wrote this, but honestly in the JASS days, TriggerEvaluate was worth something like 30 DoNothing function calls. Anitarf had a thread on wc3c about it.

Nevertheless, even in 2014 apparently the difference was only 2 function calls per trigger evaluate (according to benchmarks). There might have been a patch between 2011 and 2014 which fixed the performance issues.

I think this can be graveyarded if it's such a small difference.

Edit: Nuked
 
Last edited:
Top