• 🏆 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] Precise Wait (GUI-friendly)

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Are you frustrated with your waits taking too long to finish, finishing too early or generally being random in how long they take? This resource is the silver bullet solution to all of these problems: your GUI waits will now wait the exact length of time - no earlier and no later than expected.

It overwrites the PolledWait and TriggerSleepAction natives, making "Wait - Game Time" and "Wait" in GUI simply do just that.

The original idea behind PreciseWait is credited towards @DoctorGester. I was inspired to overwrite the actual GUI function after I saw an implementation from @MindWorX, who had shown it working in TypeScript.

This is completely GUI-friendly, so GUI triggers can just use Wait as they normally would.


Edit 8 July 2019: For reasons related to compatibility, it is also necessary to override the default scripts which normally sleep the thread (ie. TriggerSleepAction and SyncSelections). It is safe to do either one from an ExecuteFunc thread, so I've updated the script. Regular sleep now defaults to the same PolledWait behavior.

Edit 10 July 2019 - Added compatibility to force-enable Wait functions even when using Lua Fast Triggers. Bear in mind the function "EnableWaits" is actually found within the Fast Triggers script and not this one.

Edit 7 April 2022 - Updated requirement to new version of TimerUtils, opened up the API to allow the user to call the original TriggerSleepAction if they wish.

Edit 16 April 2022 - Updated requirement to use Hook.

Edit 18 April 2022 - Updated to require Hook 3.4.

Edit 22 April 2022 - No longer requires TimerUtils.

Edit 5 May 2022 - Now works correctly with the newer version of Hook. Thanks @Jampion !

Edit 6 June 2022 - Updated to use Hook 5.0, add support for "WaitIndex" which Global Variable Remapper turns into returning the value of "coroutine.running()" in order to ensure the index is unique to that set of triggeractions. The cleanup is tricky with this without setting values to nil, so I recommend using GUI Repair Kit to make sure your arrays (tables) get cleaned up when the thread execution has ended.

Edit 7 July 2022 - Fixed the issue created when switching to coroutine.wrap, as I had mistakenly thought the function would create a new thread each time (it doesn't, so I've reverted it). Also fixed the issue where the return value of the hooked triggeraction was forgotten.

5 Oct 2022 - Now uses OnLibraryInit and treats AddHook as an optional requirement.

17 Oct 2022 - Now uses the Require functionality added to Global Initialization instead of OnLibraryInit.


Lua:
OnInit("PreciseWait", function(require) --https://www.hiveworkshop.com/threads/total-initialization.317099/

    local hook  = require.optionally "AddHook"     --https://www.hiveworkshop.com/threads/hook.339153
    local remap = require.optionally "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper-the-future-of-gui.339308/

    --Precise Wait v1.5.0.1
    --This changes the default functionality of TriggerAddAction, PolledWait
    --and (because they don't work with manual coroutines) TriggerSleepAction and SyncSelections.
    
    local _ACTION_PRIORITY  =  1 --Specify the hook priority for hooking TriggerAddAction (higher numbers run earlier in the sequence).
    local _WAIT_PRIORITY    = -2 --The hook priority for TriggerSleepAction/PolledWait
    
    local function wait(duration)
        local thread = coroutine.running()
        if thread then
            local t = CreateTimer()
            TimerStart(t, duration, false, function()
                DestroyTimer(t)
                coroutine.resume(thread)
            end)
            coroutine.yield(thread)
        end
    end

    if remap then
        --This enables GUI to access WaitIndex as a "local" index for their arrays, which allows
        --the simplest fully-instanciable data attachment in WarCraft 3's GUI history. However,
        --using it as an array index will cause memory leaks over time, unless you also install
        --Lua-Infused GUI: https://www.hiveworkshop.com/threads/lua-infused-gui-automatic-group-location-rect-and-force-leak-prevention.317084/

        remap("udg_WaitIndex", coroutine.running)
    end
    if not hook then
        hook = function(varName, userFunc)
            local old = rawget(_G, varName)
            rawset(_G, varName, userFunc)
            return old
        end
    end
    
    hook("PolledWait", wait, _WAIT_PRIORITY)
    hook("TriggerSleepAction", wait, _WAIT_PRIORITY)
    
    local oldSync
    oldSync = hook("SyncSelections",
    function()
        local thread = coroutine.running()
        if thread then
            function SyncSelectionsHelper() --this function gets re-declared each time, so calling it via ExecuteFunc will still reference the correct thread.
                oldSync()
                coroutine.resume(thread)
            end
            ExecuteFunc("SyncSelectionsHelper")
            coroutine.yield(thread)
        end
    end)
    
    local oldAdd
    oldAdd = hook("TriggerAddAction", function(trig, func)

        --Return a function that will actually be added as the triggeraction, which itself wraps the actual function in a coroutine.
        return oldAdd(trig, function() coroutine.resume(coroutine.create(func)) end)
    end, _ACTION_PRIORITY)
    
end)

Example usage:

  • NormalTrigger
    • Events
    • Conditions
    • Actions
      • Wait - 3.00 seconds of game time (waits exactly 3 seconds)
      • Wait - 0.03 seconds of game time (waits exactly 0.03 seconds - previously not possible to have waits be this short)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Tested this with TimerUtils [Lua], and this is crazy!

A perfect polled wait could be considered a dream come true for GUI users (since they won't have to create separate triggers just for timer management), and suffice to say, this will be enough to deprecate a lot of timer attachment resources.

Great Job!
Thank you - I'm glad this has the potential to make a difference!

I added some stuff to the script to compensate for issues like SyncSelections crashing the thread. Also, the vanilla TriggerSleepAction is now doing the exact same thing as a PolledWait (otherwise the thread crashes).
 
Level 2
Joined
Jul 23, 2019
Messages
7
Hey,

so i just imported your LUA Code into my map. I dont know why, but many of my triggers with waits in it dont seem to work anymore.

The moment i deactivate it everything runs fine, any ideas why this is happening? Do you have any example maps for this?

Thanks in advance
 
Last edited:

Bribe

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

so i just imported your LUA Code into my map. I dont know why, but many of my triggers with waits in it dont seem to work anymore.

The moment i deactivate it everything runs fine, any ideas why this is happening? Do you have any example maps for this?

Thanks in advance

The Lua Damage Engine map is what I use to test all this stuff and you can find it all in use there (though I might have a few tests disabled).

Are you able to provide me examples of your triggers where the waits don’t work? Also if you could please let me know what other Lua resources you use (besides Lua Timer Utils).
 
Level 2
Joined
Jul 23, 2019
Messages
7
Hey, thanks for replying.

My Editor is in German, so im sorry for that. Attached is a propably really bad trigger, but i think it is a good example.

It should fire when a unit is casting a certain spell. Then it waits for 0.75 Seconds and casts the spell again. This trigger worked without any problems for years.

Trigger.png


The posted trigger does not work at all when your LUA Code is active. (I saw you using the 'Wait x seconds of game time' action, so i tried changing this aswell but it did nothing)

Did i import your Script the right way? Im really not shure if i did it right.

Lua Timer.png



Im using the Garbage Collection Script from Dr Super Good, but i already deactivated it for testing and the problem was still there.

Thanks for you help.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The issue is that you need to set event responses before doing the wait.

Set TempCaster = (Triggering unit)

I should improve the documentation to note this stuff. I've been inactive the past couple weeks and I'll resume my work in the second week of August when I am back from vacation.

Keep in mind with this you may want to do stuff like consider making it MUI. You can do it with local variables or indexing.
 
Last edited:
Level 2
Joined
Jul 23, 2019
Messages
7
Oh wow, i have to rework so much things now. But there is no way around i think, the waits became so weird since the new wc3 version. I guess thats the price i have to pay.

And again, thank you very much.
 
Level 4
Joined
Apr 11, 2018
Messages
24
Would you add "WaitIndex" global variable that could be used to save data for each next wait execution? Like we did it with timers "GetHandeId(timer)". Easier and may be cheaper than overriding all the thread functions such as "GetTrigger..."
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Would you add "WaitIndex" global variable that could be used to save data for each next wait execution? Like we did it with timers "GetHandeId(timer)". Easier and may be cheaper than overriding all the thread functions such as "GetTrigger..."
That's actually a really good idea. I'll upload a demo map later which introduces a udg_ThreadIndex global variable so users can store such data into an array. I'll recycle the index like with vJass structs.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Code:
    local oldTSA = TriggerSleepAction
    function TriggerSleepAction(duration) PolledWait(duration) end

Unless i missunderstand or miss something you're breaking the main feature of TSA, which is to continue to expire while game is paused, unlike timers
 
Level 20
Joined
Jul 10, 2009
Messages
477
I can't get this to work. Code after the Polled Wait just doesn't execute at all.

Example:
Lua:
function testPolledWait()
    print("Before Polled Wait")
    PolledWait(2.)
    print("After Polled Wait")
end
Adding this function to any trigger and execute the trigger will only show the first message.

Reason seems to be that blizzard coroutines can't be yielded (anymore?). You can even ask the running coroutine, if it is yieldable and it answers "false". If you still try, it wil raise an error:
Lua:
--do this in any function:
c = coroutine.running()
print(coroutine.isyieldable(c)) --> will print false
coroutine.yield(c) --doesn't work and raises the error "attempt to yield from blizzard coroutine"
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I can't get this to work. Code after the Polled Wait just doesn't execute at all.

Example:
Lua:
function testPolledWait()
    print("Before Polled Wait")
    PolledWait(2.)
    print("After Polled Wait")
end
Adding this function to any trigger and execute the trigger will only show the first message.

Reason seems to be that blizzard coroutines can't be yielded (anymore?). You can even ask the running coroutine, if it is yieldable and it answers "false". If you still try, it wil raise an error:
Lua:
--do this in any function:
c = coroutine.running()
print(coroutine.isyieldable(c)) --> will print false
coroutine.yield(c) --doesn't work and raises the error "attempt to yield from blizzard coroutine"
You’re probably using Lua Fast Triggers or something similar in your map that prevents the code from running. This script was tested before I published it.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I guess this is mostly intended for using waits in GUI? Still, it's important to clarify in the description where this will work. e.g. only TriggerAction
Some blizzard.j functions like unit transmissions use waits, so there are cases where one might want to use waits in normal lua code.

From the submission rules:
All existing functions that may be overridden by your submission must be specified. Additionally, if there are compatibility concerns with other overrides of the same function, they should also be stated.
In most cases overriding won't cause problems, because only new functionality is added. For example:
Lua:
    local oldWait = PolledWait
    function PolledWait(duration)
        print("Wait!")
        oldWait(duration)
    end

However in this case PolledWait is overridden, without reusing what PolledWait previously did (at least in the normal path of the function):
Lua:
    local oldWait = PolledWait
    function PolledWait(duration)
        local thread = coroutine.running()
        if thread then
            TimerStart(NewTimer(thread), duration, false, function()
                coroutine.resume(ReleaseTimer())
            end)
            coroutine.yield(thread)
        else
            oldWait(duration)
        end
    end
As a result, if the user overrides PolledWait first with something and then lets this resource override it, the first change will be lost. That's what I meant in the rules by "compatibility concerns". So in this case it's important to document that previous overrides to PolledWait and TriggerSleepAction
will be lost. The user can then adjust the order in which the function is overridden or manually adjust the overrides.

I've been using this for a while for cinematics (GUI is so so much more convenient for cinematics) and it's really good that you can simply remove the disadvantages of certain GUI functions with lua.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've updated the library to version 1.1 to take advantage of TimerUtils 2.0 and to implement the feedback suggested by @Jampion

I realize now that I never even needed to use Get/SetTimerData for this. The local variables feed into each timer in their own individual way, in a non-overwriting fashion. I really underestimated how much Lua is capable of. Nevertheless, it is still good to use the TimerUtils library due to the recycled timers/reduced code length.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
The local oldAction needs to be declared before it is used inside the hook, otherwise oldAction will simply be nil inside the hook (it's trying to find the global variable oldAction, because the local does not exist inside its own declaration) and no actions will be added to the trigger.
Lua:
local oldAction = Hook.add("TriggerAddAction",
function(whichTrig, userAction)
    if not Hook.stop then
        Hook.stop = true
        return oldAction(whichTrig, function()
            coroutine.resume(coroutine.create(function()
                userAction()
            end))
        end)
    end
end)
Can be easily fixed:
Lua:
local oldAction
oldAction = Hook.add("TriggerAddAction",
...

You used Hook.stop, but it's Hook.skip.
 
Top