• 🏆 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] Repaired Timer Natives

A background on the issues of Timer Natives can be found here: Issues with timer functions

With lua, it is now possible to overwrite natives with our own copies (somewhat). With that in mind, this script fixes some of the issues addressed in that thread:

Lua:
--  Replaces the ordinary timer natives with safe counterparts
do
    OldNatives          = {}
    local Timer         = {}
    local doDebug       = nil

    OldNatives.createTimer  = CreateTimer
    function CreateTimer()
        local obj       = OldNatives.createTimer()
        Timer[obj]      = {
            hasCallback = nil,
            callback    = nil,
            looped      = nil,
            running     = nil,
            inCallback  = nil,
            pauseFlag   = 0,
            duration    = 0.,
            elapsed     = 0.
        }
        return obj
    end

    OldNatives.pauseTimer   = PauseTimer
    function PauseTimer(t)
        if Timer[t].running then
            Timer[t].running    = nil
            Timer[t].pauseFlag  = (BlzBitOr(Timer[t].pauseFlag, 1))
        end
        OldNatives.pauseTimer(t)
    end

    OldNatives.resumeTimer  = ResumeTimer
    function ResumeTimer(t)
        if not Timer[t].hasCallback then
            if doDebug then print("ResumeTimer: Attempted to resume a timer " .. I2S(GetHandleId(t)) .. " with no callback") end
            return
        elseif Timer[t].inCallback and Timer[t].looped then
            return
        end
        if not Timer[t].inCallback and not Timer[t].running then
            Timer[t].running    = 0
        end
        OldNatives.resumeTimer(t)
    end
   
    OldNatives.timerStart   = TimerStart
    function TimerStart(t, dur, looper, func)
        if not Timer[t].hasCallback then
            --  Make it come true
            Timer[t].hasCallback = 0
        end
        if not Timer[t].inCallback then
            --  Usually when a timer is created
            Timer[t].duration   = math.max(dur, 0.)
            Timer[t].looped     = looper
            Timer[t].callback   = func
            Timer[t].running    = 0

            --  Create a new function that will act as the callback
            OldNatives.timerStart(t, dur, looper, function()
                local tr = GetExpiredTimer()
       
                Timer[tr].inCallback = 0
                Timer[tr].running    = nil
                Timer[tr].callback()
       
                Timer[tr].inCallback = nil
                if Timer[tr].destroyFlag then
                    DestroyTimer(tr)
                elseif Timer[tr].tempData then
                    --  Properties of the timer were overwritten
                    OldNatives.pauseTimer(tr)
                    TimerStart(tr, Timer[tr].tempData.dur, Timer[tr].tempData.looper, Timer[tr].tempData.func)
                    Timer[tr].tempData = nil
                    Timer[tr].elapsed = 0.
                else
                    if Timer[tr].looped and ((BlzBitAnd(Timer[tr].pauseFlag, 1) ~= 0) or (BlzBitAnd(Timer[tr].pauseFlag, 2) ~= 0)) then
                        Timer[tr].pauseFlag  = BlzBitAnd(Timer[tr].pauseFlag, 0)
                        OldNatives.pauseTimer(tr)
                        TimerStart(tr, Timer[tr].duration, Timer[tr].looped, Timer[tr].callback)
                    elseif Timer[tr].looped then
                        Timer[tr].running    = 0
                    end
                    Timer[tr].elapsed = 0.
                end
            end)
        else
            if not Timer[t].tempData then
                Timer[t].tempData = {__mode='k'}
            end
            Timer[t].tempData.dur = dur
            Timer[t].tempData.looper = looper
            Timer[t].tempData.func = func
        end
    end

    OldNatives.destroyTimer = DestroyTimer
    function DestroyTimer(t)
        if not Timer[t] or Timer[t].inCallback then
            --  No need to destroy an already-destroyed timer
            if Timer[t].inCallback then
                Timer[t].onDestroyFlag  = 0
            end
            return
        end
        if Timer[t].onDestroyFlag then
            Timer[t].onDestroyFlag = nil
        end
        if Timer[t].running or Timer[t].looped then
            Timer[t].running = nil
            OldNatives.pauseTimer(t)
        end
        Timer[t]    = nil
        OldNatives.destroyTimer(t)
    end

    OldNatives.getTimeout   = TimerGetTimeout
    function TimerGetTimeout(t) return Timer[t].duration end

    OldNatives.getElapsed   = TimerGetElapsed
    function TimerGetElapsed(t) return OldNatives.getElapsed(t) + Timer[t].elapsed end

    function TimerSetRemaining(t, newR, update)
        if Timer[t].inCallback then
            if doDebug then print('TimerSetRemaining: The remaining duration of the timer ' .. I2S(GetHandleId(t)) .. ' cannot be altered while the callback function is running.') end
            return false
        end
        if not Timer[t].hasCallback then
            if doDebug then print('TimerSetRemaining: The remaining duration of the timer ' .. I2S(GetHandleId(t)) .. ' cannot be altered while the callback function is running.') end
            return false
        end
        --  Ensure that newR is not below 0.       
        newR                = math.max(newR, 0.)
        local epsilon       = OldNatives.getElapsed(t)
        local delta         = TimerGetRemaining(t) - newR
        local newDur        = Timer[t].duration - delta
        --  If delta is anything but 0., continue with TimerSetRemaining
        if delta ~= 0. then
            OldNatives.pauseTimer(t)
            TimerStart(t, newR, Timer[t].looped, Timer[t].callback)
            Timer[t].elapsed    = Timer[t].elapsed + epsilon
            if update then
                Timer[t].duration   = newDur
            else
                Timer[t].duration   = newDur + delta
            end
            Timer[t].pauseFlag  = (BlzBitOr(Timer[t].pauseFlag, 2))
            return true
        end
        return false
    end
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've gotten mostly through what I would describe as a "healthy update" to this resource:

It takes away the "OldNatives" table (as it was too generic a name for how specific this resource is) and replaced the functions with actual tables with the __call metamethod and the "old" member to reference the original native.

I've made quite a few performance improvements by getting rid of the "Timer[t]" spam (and the local reference is not only faster but cleaner to read).

I do think this should implement some sort of timer recycling, to give it some added benefits.

I am altogether not sure what the BlzBitOr functions were doing with the PauseTimer control. Is there some reason why you are not just using true/false?

I don't plan to work any longer on the below without further clarification, but hopefully this gives you an idea of what can be improved with your code (even if my approach is ultimately scrapped).

Lua:
--  Replaces the ordinary timer natives with safe counterparts
do
    local timers        = {}
    local Timer         = {}
    local doDebug       = nil
    
    local function wrap(oldFunc, chunk)
        local old = _G[oldFunc]
        local new = {old=old}
        _G[oldFunc] = new
        setmetatable(new, {__call = chunk})
    end
    
    wrap("CreateTimer", function()
        local obj       = CreateTimer.old()
        Timer[obj]      = {duration = 0, elapsed = 0}
        return obj
    end)

    wrap("PauseTimer", function(t)
        local timer = Timer[t]
        if timer.running then
            timer.running    = nil
            timer.paused  = true
        end
        PauseTimer.old(t)
    end)

    wrap("ResumeTimer", function(t)
        local timer = Timer[t]
        if not timer.callback then
            if doDebug then print("ResumeTimer: Attempted to resume a timer " .. I2S(GetHandleId(t)) .. " with no callback") end
            return
        elseif timer.inCallback and timer.looped then
            return
        end
        if not timer.inCallback and not timer.running then
            timer.running    = 0
        end
        ResumeTimer.old(t)
    end)
   
    wrap("TimerStart", function(t, dur, looper, func)
        local timer = Timer[t]
        if not timer.hasCallback then
            --  Make it come true
            timer.hasCallback = 0
        end
        if not timer.inCallback then
            --  Usually when a timer is created
            timer.duration   = math.max(dur, 0.)
            timer.looped     = looper
            timer.callback   = func
            timer.running    = 0

            --  Create a new function that will act as the callback
            TimerStart.old(t, dur, looper, function()
                timer.inCallback = 0
                timer.running    = nil
                timer.callback()
       
                timer.inCallback = nil
                if timer.destroyFlag then
                    DestroyTimer(t)
                elseif timer.tempData then
                    --  Properties of the timer were overwritten
                    PauseTimer.old(t)
                    TimerStart(t, timer.tempData.dur, timer.tempData.looper, timer.tempData.func)
                    timer.tempData = nil
                    timer.elapsed = 0.
                else
                    if timer.looped and not timer.paused then
                        PauseTimer.old(t)
                        TimerStart(t, timer.duration, timer.looped, timer.callback)
                    elseif timer.looped then
                        timer.running    = 0
                    end
                    timer.elapsed = 0.
                end
            end)
        else
            if not timer.tempData then
                timer.tempData = {__mode='k'}
            end
            timer.tempData.dur = dur
            timer.tempData.looper = looper
            timer.tempData.func = func
        end
    end)

    wrap("DestroyTimer", function(t)
        local timer = Timer[t]
        if not timer or timer.inCallback then
            --  No need to destroy an already-destroyed timer
            if timer.inCallback then
                timer.onDestroyFlag  = 0
            end
            return
        end
        if timer.onDestroyFlag then
            timer.onDestroyFlag = nil
        end
        if timer.running or timer.looped then
            timer.running = nil
            PauseTimer.old(t)
        end
        Timer[t] = nil
        DestroyTimer.old(t)
    end)

    wrap("TimerGetTimeout", function(t) return Timer[t].duration end)
    
    wrap("TimerGetElapsed", function(t) return TimerGetElapsed.old(t) + Timer[t].elapsed end
    
    function TimerSetRemaining(t, newR, update)
        local timer = Timer[t]
        if timer.inCallback then
            if doDebug then print('TimerSetRemaining: The remaining duration of the timer ' .. I2S(GetHandleId(t)) .. ' cannot be altered while the callback function is running.') end
            return
        end
        if not timer.callback then
            if doDebug then print('TimerSetRemaining: The remaining duration of the timer ' .. I2S(GetHandleId(t)) .. ' cannot be altered when there is no callback function.') end
            return
        end
        --  Ensure that newR is not below 0.       
        newR                = math.max(newR, 0.)
        local epsilon       = TimerGetElapsed.old(t)
        local delta         = TimerGetRemaining(t) - newR
        local newDur        = timer.duration - delta
        --  If delta is anything but 0., continue with TimerSetRemaining
        if delta ~= 0. then
            PauseTimer.old(t)
            TimerStart(t, newR, timer.looped, timer.callback)
            timer.elapsed    = timer.elapsed + epsilon
            if update then
                timer.duration   = newDur
            else
                timer.duration   = newDur + delta
            end
            timer.paused  = nil
            return true
        end
    end
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
On my book this seems very useful for GUI users in particularly.
Do GUI people really use timers, though? I've only seen timers used in timer events where they disable/enable the trigger as wished. Timers are a lot more frequently used/toyed with in custom script, due to the versatility of the callback function.
 
Top