• 🏆 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] Timed Call and Echo

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Lua:
--[[--------------------------------------------------------------------------------------
    Timed Call and Echo v2.0.1.0 by Bribe, special thanks to Eikonium and Jesus4Lyf
    
    Allows automatic timer tracking for the most common use cases (one-shot timers, and
    repeating timers that merge together for optimization).
----------------------------------------------------------------------------------------]]
do
    local _DEFAULT_ECHO_TIMEOUT = 0.03125
    local _EXIT_WHEN_FACTOR = 0.5 --Will potentially stop the echo before it has fully run
                                  --its course (via rounding). Set to 0 to disable. Can also override this from the Timed.echo 5th parameter.
    local zeroList, _ZERO_TIMER
    local timerLists = {}
    local insert = table.insert
    
Timed = {
    --[[--------------------------------------------------------------------------------------
        Name: Timed.call
        Args: [delay, ]userFunc
        Desc: After "delay" seconds, call "userFunc". Delay defaults to 0 seconds.
    ----------------------------------------------------------------------------------------]]
    ---@param delay? number
    ---@param userFunc function
    ---@return fun(doNotDestroy:boolean):number removalFunc -> only gets returned if the delay is > 0.
    call=function(delay, userFunc)
        if type(delay)=="function" then
            userFunc,delay=delay,userFunc
        end
        if not delay or delay <= 0 then
            if zeroList then
                insert(zeroList, userFunc)
            else
                zeroList = {userFunc}
                _ZERO_TIMER = _ZERO_TIMER or CreateTimer()
                TimerStart(_ZERO_TIMER, 0, false, function()
                    local tempList = zeroList
                    zeroList = nil
                    for _, func in ipairs(tempList) do func() end
                end)
            end
        else
            local t = CreateTimer()
            TimerStart(t, delay, false, function()
                DestroyTimer(t)
                t=nil
                userFunc()
            end)
            return function(doNotDestroy)
                local result = 0
                if t then
                    result = TimerGetRemaining(t)
                    if not doNotDestroy then
                        PauseTimer(t)
                        DestroyTimer(t)
                        t=nil
                    end
                end
                return result
            end
        end
    end,

    --[[--------------------------------------------------------------------------------------
    Name: Timed.echo
    Args: [timeout, duration,] userFunc
    Desc: Calls userFunc every "timeout" seconds until userFunc returns true.
        -> will also stop calling userFunc if the duration is reached.
        -> Returns a function you can call to manually stop echoing the userFunc.
    --------------------------------------------------------------------------------------
    Note: This merges all matching timeouts together, so it is advisable only to use this
        for smaller numbers (e.g. <.3 seconds) where the difference is less noticeable.
    ----------------------------------------------------------------------------------------]]
    ---@param timeout? number
    ---@param duration? number
    ---@param userFunc fun():boolean -- if true, echo will stop
    ---@param onExpire? function     -- If the duration is specified and expiration occurs naturally, call this function.
    ---@param tolerance? number      -- Ranges from 0-1. If the duration is specified, the tolerance helps to measure the accuracy of the final tick.
    ---@return function remove_func
    echo=function(timeout, duration, userFunc, onExpire, tolerance)
        if type(timeout) == "function" then
            --parames align to original API of (function[,timeout])
            userFunc,timeout,duration=timeout,duration,userFunc
        elseif not userFunc then
            --params were (timeout,userFunc)
            userFunc,duration=duration,nil
        --else params were exactly as defined.
        end
        local wrapper = function()
            return not userFunc or userFunc() --this wrapper function allows manual removal to be understood and processed accordingly.
        end
        timeout = timeout or _DEFAULT_ECHO_TIMEOUT
        if duration then
            local old=wrapper
            local exitwhen = timeout*(tolerance or _EXIT_WHEN_FACTOR)
            wrapper=function() --this wrapper function enables automatic removal once the duration is reached.
                if not old() then
                    duration = duration - timeout
                    if duration >= exitwhen then
                        return
                    elseif onExpire then
                        print(duration, exitwhen)
                        onExpire()
                    end
                end
                return true
            end
        else
            duration=0
        end
        local timerList = timerLists[timeout]
        if timerList then
            local remaining = TimerGetRemaining(timerList.timer)
            if remaining >= timeout * 0.50 then
                duration = duration + timeout --The delay is large enough to execute on the next tick, therefore increase the duration to avoid double-deducting.
                insert(timerList, wrapper)
            elseif timerList.queue then
                insert(timerList.queue, wrapper)
            else
                timerList.queue = {wrapper}
            end
            duration = duration - remaining --decrease the duration to compensate for the extra remaining time before the next tick.
        else
            timerList = {wrapper}
            timerLists[timeout] = timerList
            timerList.timer = CreateTimer()
            TimerStart(timerList.timer, timeout, true, function()
                local top=#timerList
                for i=top,1,-1 do
                    if timerList[i]() then --The userFunc is to be removed:
                        if i~=top then
                            timerList[i]=timerList[top]
                        end
                        timerList[top]=nil
                        top=top-1
                    end
                end
                if timerList.queue then --Now we can add the queued items to the main list
                    for i,func in ipairs(timerList.queue) do
                        timerList[top+i]=func
                    end
                    timerList.queue = nil
                elseif top == 0 then --list is empty; clear its data.
                    timerLists[timeout] = nil
                    PauseTimer(timerList.timer)
                    DestroyTimer(timerList.timer)
                end
            end)
        end
        return function(doNotDestroy)
            if not doNotDestroy then
                userFunc=nil
            end
            return duration
        end
    end
}
end
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Useful alternative to TriggerSleepAction and Timers.

Question, would there be any drawbacks from using nested Timed.calls?
For short, repeating timers (under .2 seconds), which have multiple instances running simultaneously, Timed.echo can be better. It just depends on what you're doing.

Otherwise, Timed.call is mostly inlining the timer API for convenience. It provides a good benefit for some of my other systems with providing a single zero-second timer that merges other zero-second timer callbacks with it (kind of like Timed.echo, but non-repeating).
 
I tried making a simple test Timed.echo function:
Lua:
function testEcho()
    local x = 0
    local f = 10
    
    local test = function()
        x = x+1
        print("x =="..x)
        if x == f then
            print("stop echo")
            return true
        end
    end
    print("test start")
    Timed.echo( test, 0.20 )
    print("test successful")
end

This causes Warcraft 3 to freeze at "x == 9", although I think it tried to iterate to 10 before freezing because there's a slight delay before it freezes.
I'm not sure what the problem is. Am I not stopping the echo properly?
 
Sorry, I had forgotten to get back to you on your original question!

On paper, what you are doing there looks like it should NOT cause the problem you described.

You don't need x and a f though.

Can you send me the demo map you have?
Nevermind that.

I wouldn't know. It looked like it should work just fine, right?

I don't need x and f? Can you elaborate on that?

Anyway, here's a test map:
 

Attachments

  • testEcho.w3x
    23.3 KB · Views: 4

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
I just did a pretty massive update to this library to update it to version 2.0. It no longer requires LinkedList, and now works a bit differently.

Overall, the aim here was to do a cleanup on unnecessary data and stick to just what someone might want to expect from a timer system.

@Yours Truly your "testEcho" function doesn't need to iterate with an integer, because you can track the duration. Previously, you could get the node.elapsed value in order to compare it, but now I have built the duration as an optional parameter to Timed.echo:

Lua:
OnGlobalInit(function()
    local test = function()
        print "echo is ticking"
    end
    print("test start")
    Timed.echo(0.20, 2.00, test, function() print "echo has stopped" end)
    print("test successful")
end)
 
Top