• 🏆 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
The below is untested, but I plan to test and update it tonight (CET) to publish it. The update:

1) Adds compatibility with Hook 5.0
2) If Global Variable Remapper is in the map, this then adds the GUI WaitIndex as originally-requested by GetLocalPlayer. It should be declared in the Globals Editor as a non-array integer. It allows perfect indexing as long as one uses the WaitIndex for caching things like (Triggering unit) into an array.
3) Renamed from Perfect PolledWait to Precise Wait

Lua:
if AddHook then -- https://www.hiveworkshop.com/threads/hook.339153
    --Precise Wait v1.4.0.0 beta
    --This changes the default functionality of TriggerSleepAction, PolledWait
    --TriggerAddAction and SyncSelections. If there are any natives that force
    --a thread to yield that won't work with Lua coroutines, please let me know.
    
    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
    
    AddHook("PolledWait", wait, _WAIT_PRIORITY)
    AddHook("TriggerSleepAction", wait, _WAIT_PRIORITY)
    
    local oldSync
    oldSync = AddHook("SyncSelections",
    function()
        local thread = coroutine.running()
        if thread then
            function SyncSelectionsHelper()
                oldSync()
                coroutine.resume(thread)
            end
            ExecuteFunc("SyncSelectionsHelper")
            coroutine.yield(thread)
        end
    end)
    
    local oldAdd
    local runningIndex
    oldAdd = AddHook("TriggerAddAction", function(trig, func)
        if GlobalRemap then
            if not runningIndex then
                GlobalRemap("udg_WaitIndex", function() return runningIndex end)
                runningIndex = 0
            end
            oldAdd(trig, function()
                runningIndex = runningIndex + 1
                coroutine.wrap(func)()
                runningIndex = runningIndex - 1
            end)
        else
            oldAdd(trig, coroutine.wrap(func))
        end
    end, _ACTION_PRIORITY)
    
end --End of library PolledWait

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..."
This is added in this beta version. Sorry it took so long to follow through on this.

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"
This error can occur if you are trying to use waits outside of a triggeraction or ExecuteFunc instance. These are special kinds of functions which are yieldable (but don't have coroutines wrapped around them, so I have to create them myself). BoolExpr and Code callback functions such as Filter, Condition, TimerStart, ForGroup cannot have waits in them, even with this resource.

Nevertheless, you did discover that function containers preserve their local variables even after a TimerStart has expired, which likely solved the issue you were trying to work out. That discovery is huge and incredibly amazing at getting me to re-tool pretty much all of my resources to benefit from it. I feel like I am just scratching the surface.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
Lua:
local oldAdd, init
oldAdd = AddHook("TriggerAddAction", function(trig, func)
    if GlobalRemap and not init then
        init = true
        GlobalRemap("udg_WaitIndex", function() return coroutine.running() end)
    end
    oldAdd(trig, coroutine.wrap(func))
end, _ACTION_PRIORITY)
This part seems to break basic spell events:
Lua:
do
    local t = CreateTrigger()
            TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            TriggerAddAction(t, function ()
                print("OnCast")
            end)
end
This prints OnCast only once. Further spell events don't work.

With the old coroutine wrapper it works:
Lua:
oldAdd(trig, function()
      coroutine.resume(coroutine.create(function()
            func()
      end))
end)

Additionally, you should return the result of oldAdd. TriggerAddAction returns triggeraction.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
This part seems to break basic spell events:
Lua:
do
local t = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
TriggerAddAction(t, function ()
print("OnCast")
end)
end
This prints OnCast only once. Further spell events don't work.
This problem happened to me (I case you create the spells in the way you are showing), the reason is because I created the trigger and register the event in the main code and not within a OnTrigInit block, they told me something about the garbage collector collecting the trigger and/or the event.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Now I have a problem for some reason for having this system when I start the map it displays every tick an error message of "cannot resume dead courutine" and I don't know what and where is prvoking this, it only displays that message without any extra information, I don't even could localize the root of the problem.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I'm glad you're back, @Jampion, as I was just churning these things out as I came up with more crazy ideas and didn't test them as thoroughly as you did.

I thought I was being rather clever with simply createing a coroutine function and using it as the action, but I missed the critical part where that function is not re-creating a coroutine each time it is called, so once it is called the first time, it can never be called again. I've therefore reverted it back to the original, as coroutine.wrap creates an extra function handle and create/resume are probably more efficient by not doing that.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
This part seems to break basic spell events:
Lua:
do
    local t = CreateTrigger()
            TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            TriggerAddAction(t, function ()
                print("OnCast")
            end)
end
This prints OnCast only once. Further spell events don't work.
Isn't this the result of creating handles on lua root? it's just getting garbage collected.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Isn't this the result of creating handles on lua root? it's just getting garbage collected.
Not in this scenario. The root cause was my incorrect assumption that coroutine.wrap would perpetually create a new coroutine when that function is called (when in fact it dies after one use, making it incredibly useless).
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
"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.
Can you explain why there are problems with cleanup? Does coroutine.running() return something that needs to be cleaned up?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Can you explain why there are problems with cleanup? Does coroutine.running() return something that needs to be cleaned up?
The Lua garbage collector will only pick up items that no longer have references pointing to them, so when you are using the thread object as a reference (which is what is returned by coroutine.running()) you need to make sure that that key gets erased at some point. The way to automate this is by using __mode = "k" within a metatable, which is handled by [Lua] - GUI Repair Collection.

My vision is for GUI to do something like this:

  • My Trig
    • Events
    • Conditions
    • Actions
      • Set MyUnit[WaitIndex] = (Some unit)
      • For each (Integer A) from 1 to 100 do (Multiple Actions)
        • Loop - Actions
          • Wait - 0.03 Seconds
          • -------- Do some projectile movement or knockback stuff --------
          • Unit - Move MyUnit[WaitIndex] instantly to (some position) facing Default building facing degrees
This absolutely slaughters all other indexing systems by giving GUI users access to what can be considered "local GUI variables". Arrays with coroutine object references are comparably as powerful to GUI as what "local" is to someone writing in Lua directly (both of which being much more powerful than anything vJass was ever able to deliver).
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
So it's just a problem if you use the value returned by WaitIndex as array/table index?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
So it's just a problem if you use the value returned by WaitIndex as array/table index?
Correct. I could alternatively do some kind of integer-based struct indexing that is used to represent the WaitIndex object, but it would need an incremental timer to check for when the status of the coroutine becomes "dead". WarCraft 3 does not allow hooking of the garbage collector API, so in this sense I could not rely on the __mode="k" metatable to clean that struct integer.

So ultimately, two schools of thought: let GUI Repair Collection fix the issue created by this problem, or use integer representation (which would ultimately use more memory while waiting for integer data to replace itself).

Therefore, the current strategy of indexing by the thread object as a key is the least memory-intensive and easiest to implement.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Is there any way to use this without replacing the original functions? I'd like to just call this function instead.
If you are coding within Lua, you don't need to do this sort of thing, as you can use native timers and their callback functions will inherit the locals from the parent function as upvalues.

If you prefer to use fewer lines of code to achieve the same results, I can recommend using Timed.call, which hides the timer API and allows you to focus on getting to the next block of your code.

If you really just want to yield, you need to make sure you are within a yieldable coroutine and can just call PolledWait from there.
 
If you are coding within Lua, you don't need to do this sort of thing, as you can use native timers and their callback functions will inherit the locals from the parent function as upvalues.

If you prefer to use fewer lines of code to achieve the same results, I can recommend using Timed.call, which hides the timer API and allows you to focus on getting to the next block of your code.

If you really just want to yield, you need to make sure you are within a yieldable coroutine and can just call PolledWait from there.
I'm actually just starting to learn Lua, so I was unaware of all the other alternatives. I'll search up how to do those, thanks.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Update: This now takes advantage of the optional requirements and library name feature introduced in Global Initialization 4.1.

The optional requirements are AddHook and GlobalRemap.

Resources can now require this via its initialization-specific name "PreciseWait". Action and CreateEvent are two resources that have needed this library, but didn't have a proper way to identify it in hard code until now.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This looks very good and useful.
Though I heard tha Lua can't be used with Jass, since its the map settings and all.
Is it possible to use this with GUI+JASS?
This resource is one of the main reasons why Lua is so much better for GUI users: because you can change the behavior of native functions (and even GUI variables). This sort of thing is impossible in GUI when the map is configured for JASS.

Most of the big vJass libraries already have Lua versions of themselves at this point. For vJass spells, you can use vJass2Lua.

Lua-Infused GUI also makes it so you can delete all of the "RemoveLocation" and "DestroyGroup" custom script from your map, because it automatically handles memory for you.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Seeing this system I'm noticing that using it in a if p == GetLocalPlayer() then would cause a desync, is there a way to do a similar system that does this in those blocks?
If the timers were recycled, it wouldn't desync. But I think a timer recycling system should be its own separate resource.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I am not sure now that you mention it. If the timers are preloaded into a synchronous stack, why would TimerStart cause a desync as long as there are always timers available asynchronously.


But the TimerStart function doesn't cause a desync if is called in the if p == GetLocalPlayer() then block?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Precise Wait has been updated to a new version which now only optionally requires Total Initialization:
- Lua/Precise Wait.lua at master · BribeFromTheHive/Lua

I've composed some unit tests for it that can be run in ZeroBrane Studio here:
- Lua/tests/PreciseWait Tests.lua at master · BribeFromTheHive/Lua

I'll get around to updating Total Initialization eventually. During unit testing for this, I discovered a bug which may even exist in the released version of Total Initialization.

Once Total Initialization is fully tested and updated, I'll update the top thread here with the latest version of Precise Wait.
 
Top