• 🏆 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!

[System] Timer Tools

Level 31
Joined
Jul 10, 2007
Messages
6,306
So.. I did a v3 for Timer Tools with a super simplified API. It is completely untested, but you can try it out if you like. The code is much easier to navigate and read as well. I haven't added in the modules yet ; ).

Max timeout is like 25 seconds. After that, it's just better to use regular timers because the chance of merging is too small ;p.

I'll test this out later and get it working =). I'll also add in the modules ;p.

JASS:
/*
3.0.0.0

struct Timer extends array
    readonly static Timer expired   //CALL EXACTLY ONCE!
    readonly real remaining
    readonly real elapsed
    readonly real timeout
    
    method createDialog takes nothing returns timerdialog

    static method create takes real timeout, code onExpire returns Timer
    method destroy takes nothing returns nothing
*/

library TimerTools
    /*
    *   Checks if any CTM modules appear to be constant
    */
    static if DEBUG_MODE then
        private struct DEBI extends array
            static constant boolean SUGGEST_MODULE_CHANGES = false
        endstruct
    endif
    
    globals
        /*************************************************************************************
        *
        *                    RELATIVE_MERGE
        *
        *    Effects the accuracy of first tick. The smaller the merge value, the less chance two
        *    timers have of sharing the same first tick, which leads to worse performance.
        *    However, a larger merge value decreases the accuracy of the first tick.
        *
        *    Formula: Ln(RELATIVE_MERGE/timeout)/230.258509*3600*timeout
        *        Ln(64000/3600)/230.258509*3600=44.995589035797596625536391805959 seconds max off
        *
        *************************************************************************************/
        private constant real RELATIVE_MERGE =                  64000

        /*************************************************************************************
        *
        *                    CONSTANT_MERGE
        *
        *    Effects the accuracy of the first tick. This is a constant merge. If this value +.01 is
        *    greater than the value calculated from RELATIVE_MERGE, the timer auto merges.
        *
        *    Constant merge must be greater than 0.
        *
        *************************************************************************************/
        private constant real CONSTANT_MERGE =                  .1
    endglobals
    
    private keyword TimerNode
    private keyword TimerGroup

    globals
        private code expirationCode
    endglobals
    
    //! textmacro T_ALLOC
        private static integer instanceCount = 0
        private static integer array recycler
        
        private static method allocate takes nothing returns thistype
            local thistype this = recycler[0]
            
            if (0 == this) then
                set instanceCount = instanceCount + 1
                debug set recycler[instanceCount] = -1
                return instanceCount
            endif
            
            set recycler[0] = recycler[this]
            debug set recycler[this] = -1
            
            return this
        endmethod
        
        private method deallocate takes nothing returns boolean
            debug if (recycler[this] == -1) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,10,"DOUBLE FREE ERROR: TIMER " + StringCase("", true))
                debug return false
            debug endif
            
            set recycler[this] = recycler[0]
            set recycler[0] = this
            
            return true
        endmethod
    //! endtextmacro
    
    //hiveworkshop.com/forums/jass-functions-413/snippet-natural-logarithm-108059/
    //credits to BlinkBoy
    private function Ln takes real a returns real
        local real s = 0
        
        loop
            exitwhen a < 2.71828
            
            set a = a/2.71828
            set s = s + 1
        endloop
        
        return s + (a - 1)*(1 + 8/(1 + a) + 1/a)/6
    endfunction
    
    private module Queue
        thistype first
        thistype last
        
        thistype next
        
        thistype head
        
        method add takes thistype node returns nothing
            if (0 == first) then
                set first = node
            else
                set last.next = node
            endif
            
            set last = node
            set node.next = 0
            
            set node.head = this
        endmethod
        
        method clear takes nothing returns nothing
            set first = 0
            set last = 0
        endmethod
    endmodule
    
    /*
    private module SingleQueue
        static thistype last
        thistype next
        
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
        
        static method add takes thistype this returns nothing
            set next = 0
            set last.next = this
            set last = this
        endmethod
        
        static method clear takes nothing returns nothing
            set last = 0
            set thistype(0).next = 0
        endmethod
    endmodule
    */
    
    private module SingleStack
        thistype next
            
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
        
        static method push takes thistype this returns nothing
            set next = thistype(0).next
            set thistype(0).next = this
        endmethod
        
        static method clear takes nothing returns nothing
            set thistype(0).next = 0
        endmethod
    endmodule
    
    private module CircularList
        thistype first
        thistype next
        thistype prev
        
        thistype head
        
        debug private boolean added
        
        method operator last takes nothing returns thistype
            return first.prev
        endmethod
    
        method add takes thistype node returns nothing
            debug if (0 != node.head) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,10,"T CIRCULAR LIST ERROR: ATTEMPT TO ADD ALREADY ADDED NODE TO LIST")
                debug set node = 1/0
            debug endif
            
            if (0 == first) then
                set first = node
                set node.next = node
                set node.prev = node
            else
                set node.next = first
                set node.prev = first.prev
                set node.prev.next = node
                set node.next.prev = node
            endif
            
            set node.head = this
        endmethod
        
        method remove takes nothing returns boolean
            local thistype node = this
            set this = node.head
            
            debug if (0 == this) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,10,"T CIRCULAR LIST ERROR: ATTEMPT TO REMOVE ALREADY REMOVE NODE FROM LIST")
                debug set node = 1/0
            debug endif
            
            set node.prev.next = node.next
            set node.next.prev = node.prev
            
            if (first == node) then
                set first = node.next
                
                if (first == node) then
                    set first = 0
                endif
            endif
            
            set node.next = 0
            set node.prev = 0
            
            set node.head = 0
            
            return 0 == first
        endmethod
    endmodule
    
    /*
    private module List
        thistype first
        thistype last
        
        thistype next
        thistype prev
        
        thistype head
        
        method add takes thistype node returns nothing
            debug if (0 != node.head) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,10,"T LIST ERROR: ATTEMPT TO ADD ALREADY ADDED NODE TO LIST")
                debug set node = 1/0
            debug endif
        
            if (0 == first) then
                set first = node
                set last = node
            else
                set last.next = node
                set node.prev = last
                
                set last = node
            endif
            
            set node.head = this
        endmethod
        
        method remove takes nothing returns boolean
            local thistype node = this
            set this = node.head
        
            debug if (0 == this) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,10,"T LIST ERROR: ATTEMPT TO REMOVE ALREADY REMOVE NODE FROM LIST")
                debug set node = 1/0
            debug endif
            
            if (first == node) then
                set node.next.prev = 0
                
                set first = node.next
            else
                set node.next.prev = node.prev
            endif
            
            if (last == node) then
                set node.prev.next = 0
                
                set last = node.prev
            else
                set node.prev.next = node.next
            endif
            
            set node.next = 0
            set node.prev = 0
            
            set node.head = 0
            
            return 0 == first
        endmethod
    endmodule
    */
    
    /*
    *   This stores timers that are to be added to a sublist of a timer group after
    *   that sublist expires.
    *
    *   This is iterated over after a sublist expires
    */
    private struct TimerAddQueue extends array
        implement Queue
    endstruct
    
    /*
    *   This stores timers to be destroyed. This is iterated over after any sublist expires.
    */
    private struct TimerDestructionStack extends array
        implement SingleStack
    endstruct
    
    /*
    *   A sublist is a list of actual timer nodes that expire at the same time
    */
    private struct TimerSubGroup extends array
        implement CircularList
        
        //! runtextmacro T_ALLOC()
        
        timer timer
        trigger trigger
        boolean running
        
        method fire takes nothing returns nothing
            call TriggerEvaluate(trigger)
        endmethod
        
        method get takes nothing returns thistype
            local thistype node = first
            set first = first.next
            return node
        endmethod
        
        method operator queue takes nothing returns TimerAddQueue
            return this
        endmethod
        
        static method operator stack takes nothing returns TimerDestructionStack
            return 0
        endmethod
    
        static method create takes real timeout returns thistype
            local thistype this = allocate()
            
            if (null == timer) then
                set timer = CreateTimer()
                set trigger = CreateTrigger()
            endif
            
            call TimerStart(timer, timeout, true, expirationCode)
            
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            call deallocate()
            
            call TriggerClearConditions(trigger)
            call PauseTimer(timer)
        endmethod
        
        method register takes TimerNode node returns triggercondition
            call add(node)
        
            return TriggerAddCondition(trigger, node.code)
        endmethod
        
        method unregister takes TimerNode node returns boolean
            call TriggerRemoveCondition(trigger, node.registered)
            
            return thistype(node).remove() and 0 == TimerAddQueue(this).first
        endmethod
    endstruct
    
    private struct TimerNode extends array
        //! runtextmacro T_ALLOC()
        
        boolexpr code
        readonly triggercondition registered
        readonly TimerSubGroup owner
        
        private method register takes nothing returns nothing
            set registered = owner.register(this)
        endmethod
        
        static method create takes boolexpr codeToRegister, TimerSubGroup owner, boolean addAfter returns thistype
            local thistype this = allocate()
            
            set code = codeToRegister
            set this.owner = owner
            
            if (addAfter) then
                call owner.queue.add(this)
            else
                call register()
            endif
            
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            call deallocate()
            
            if (null != registered) then
                if (owner.unregister(this)) then
                    call owner.destroy()
                endif
                
                set registered = null
            endif
            
            set code = null
            set owner = 0
        endmethod
        
        private static method registerNodes takes TimerAddQueue queue returns nothing
            local thistype this = queue.first
            
            loop
                exitwhen 0 == this
                
                if (null == code) then
                    call deallocate()
                else
                    call register()
                endif
                
                set this = TimerAddQueue(this).next
            endloop
            
            call queue.clear()
        endmethod
        
        private static method destroyNodes takes nothing returns nothing
            local thistype this = TimerDestructionStack.first
            
            loop
                exitwhen 0 == this
                
                call destroy()
                
                set this = TimerDestructionStack(this).next
            endloop
            
            call TimerDestructionStack.clear()
        endmethod
        
        static method cleanup takes TimerAddQueue queue returns nothing
            call registerNodes(queue)
            call destroyNodes()
        endmethod
    endstruct
    
    /*
    *   This stores a list of sublists that have the same timeouts
    */
    private struct TimerGroup extends array
        implement CircularList
    
        readonly boolean permanent
        readonly real threshold
        readonly real timeout
        
        readonly static TimerSubGroup expired = 0
        
        private method operator empty takes nothing returns boolean
            return 0 == first
        endmethod
        
        private method operator addBeforeFirst takes nothing returns boolean
            return permanent or TimerGetElapsed(TimerSubGroup(first).timer) <= threshold
        endmethod
        
        private method operator addBeforeLast takes nothing returns boolean
            return TimerGetElapsed(TimerSubGroup(last).timer) <= threshold
        endmethod
        
        private method operator addAfterLast takes nothing returns boolean
            return TimerGetRemaining(TimerSubGroup(last).timer) <= threshold
        endmethod
        
        static method getTimerGroup takes real timeout returns thistype
            local integer i = R2I(timeout) * 320
            
            if (0 == i) then
                return 1
            elseif (8191 < i) then
                return 8191
            endif
            
            return i
        endmethod
        
        private method getExpired takes nothing returns TimerSubGroup
            local TimerSubGroup expired = first
            set first = first.next
            return expired
        endmethod
        
        private static method onExpire takes nothing returns nothing
            local thistype this = getTimerGroup(TimerGetTimeout(GetExpiredTimer()))
            local TimerSubGroup expired = getExpired()
            
            set thistype.expired = expired
            
            set expired.running = true
            call expired.fire()
            set expired.running = false
            
            set thistype.expired = 0
            
            call TimerNode.cleanup(expired)
            
            if (0 == expired.first) then
                call expired.remove()
            endif
        endmethod
        
        static method operator [] takes real timeout returns thistype
            local thistype timerGroup = getTimerGroup(timeout)
            
            if (0 == timeout) then
                set timeout = timerGroup/320.
                
                set timerGroup.threshold = Ln(RELATIVE_MERGE/timerGroup.timeout)/230.258509*timerGroup.timeout
                if (timerGroup.threshold < CONSTANT_MERGE) then
                    set timerGroup.threshold = CONSTANT_MERGE
                endif
                
                set timerGroup.permanent = CONSTANT_MERGE < (timerGroup.timeout - timerGroup.threshold)/2
            endif
            
            return timerGroup
        endmethod
        
        method register takes boolexpr onExpire returns thistype
            local TimerSubGroup subgroup
            local TimerNode node
        
            if (empty) then
                set subgroup = TimerSubGroup.create(timeout)
                set node = TimerNode.create(onExpire, subgroup, false)
                
                call add(subgroup)
            elseif (addBeforeFirst) then
                set subgroup = first
                set node = TimerNode.create(onExpire, subgroup, false)
            elseif (addBeforeLast) then
                set subgroup = last
                set node = TimerNode.create(onExpire, subgroup, false)
            elseif (addAfterLast) then
                set subgroup = last
                set node = TimerNode.create(onExpire, subgroup, true)
            else
                set subgroup = TimerSubGroup.create(timeout)
                set node = TimerNode.create(onExpire, subgroup, false)
                
                call add(subgroup)
            endif
        
            return node
        endmethod
        
        method destroy takes nothing returns nothing
            local TimerNode node = this
            local TimerSubGroup owner = node.owner
            set this = thistype(owner).head
            
            if (owner.running) then
                if (null == node.registered) then
                    set node.code = null
                else
                    call TimerDestructionStack.push(node)
                endif
            else
                call node.destroy()
                
                if (0 == owner.first and 0 == owner.queue.first) then
                    call thistype(owner).remove()
                endif
            endif
        endmethod
        
        private static method onInit takes nothing returns nothing
            set expirationCode = function thistype.onExpire
        endmethod
    endstruct
    
    struct Timer extends array
        /*
        *   CALL ONCE!
        */
        static method operator expired takes nothing returns thistype
            return TimerGroup.expired.get()
        endmethod
        
        method operator remaining takes nothing returns real
            return TimerGetRemaining(TimerNode(this).owner.timer)
        endmethod
        method operator elapsed takes nothing returns real
            return TimerGetElapsed(TimerNode(this).owner.timer)
        endmethod
        method operator timeout takes nothing returns real
            return TimerGetTimeout(TimerNode(this).owner.timer)
        endmethod
        method createDialog takes nothing returns timerdialog
            return CreateTimerDialog(TimerNode(this).owner.timer)
        endmethod
    
        static method create takes real timeout, code onExpire returns thistype
            return TimerGroup[timeout].register(Condition(onExpire))
        endmethod
        
        method destroy takes nothing returns nothing
            call TimerGroup(this).destroy()
        endmethod
    endstruct
endlibrary


And here is the part that I'm sure everyone is curious about
JASS:
        private static method onExpire takes nothing returns nothing
            local thistype this = getTimerGroup(TimerGetTimeout(GetExpiredTimer()))
            local TimerSubGroup expired = getExpired()
            
            set thistype.expired = expired
            
            set running = true
            call expired.fire()
            set running = false
            
            set thistype.expired = 0
            
            call TimerNode.cleanup(expired)
            
            if (0 == expired.first) then
                call expired.remove()
            endif
        endmethod


Anyways, time to walk through the code.

There are a few main structs
JASS:
private struct TimerAddQueue extends array
private struct TimerDestructionStack extends array
private struct TimerSubGroup extends array
private struct TimerNode extends array
private struct TimerGroup extends array

struct Timer extends array

The Timer struct is how users interact with the system.

TimerGroup refers to all timers that have the same timeout. It puts these timers into subgroups of type TimerSubGroup. Each subgroup is a list of timers that all expire at the exact same time (merged timers). These subgroups each have 1 trigger + 1 list. The list is for retrieving expired timers in the subgroup (all unique ids, not to worry) and the trigger is used to evaluate all of the timers in the subgroup (code for each timer added to the trigger).

When a timer expires, the timer group is retrieved. From here, the expiring subgroup is the first subgroup in the timer group's list. The timer group's list is rotated.

Whenever Timer.expired is used, it rotates the subgroup's list so that each individual timer node can be retrieved.

When a timer is created, 5 possible things can happen.
Case 1: the subgroup list is empty, so a new subgroup is created and added to the list and the timer is put on to the subgroup
Case 2: the first subgroup is going to expire at around the same time the new timer will expire, so the new timer is added to first subgroup
Case 3: the last subgroup is going to expire at around the same time the new timer will expire, so the new timer is added to the last subgroup
Case 4: the last subgroup is just about to expire, so its next expiration will expire around when the new timer will expire, so the new timer is thrown into a queue to be added after the subgroup's expiration
Case 5: there are no subgroups that are going to expire anywhere near the new timer, so a new subgroup is made for the new timer and the timer is added to it

When a timer is destroyed, 3 possible things can happen
Case 1: if the timer's subgroup is running and the timer is going to be added to the subgroup, set it to not add by setting its code to null
Case 2: if the timer's subgroup is running and the timer is on the subgroup, add it to the destruction stack to be destroyed after the subgroup is done running
Case 3: if the timer's subgroup is not running, destroy the timer normally

When a timer is destroyed, if that subgroup is empty and its queue of timers to add is empty, destroy the subgroup and remove it from the timer group list.


To keep the code simple, only certain structs interact with other structs and modify data in those other structs. If they all modified data everywhere, it would turn into a total mess.

subgroups handle nothing
TimerNode primarily handles subgroups + contains methods for cleaning timer nodes
TimerGroup primarily handles timer nodes
Timer primarily handles TimerGroup
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Timer Tools 3 is now released

have fun

edit
this is the code I used to squash every bug in the system : p

JASS:
struct Tester extends array
    private static hashtable testTable = InitHashtable()
    private static timer array testArray
    private static boolexpr c
    private static code m
    private static integer funcId
    
    private static integer count = 0
    
    private static boolean enabled = true
    
    implement TimerHead
    
    private static method test takes nothing returns nothing
        local timer t
        local real timeout = GetRandomReal(.01, 1.5)
        local Timer t2 = Timer.create(timeout, c, funcId)
        if (testArray[t2] != null) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ALLOCATION ERROR")
            set enabled = false
            set timeout = 1/0
        else
            set t = CreateTimer()
        endif
        call TimerStart(t, timeout, true, null)
        call SaveInteger(testTable, GetHandleId(t), 0, t2)
        set testArray[t2] = t
        set t = null
        set count = count + 1
        call add(t2)
    endmethod
    
    private static method validate takes nothing returns nothing
        local Timer node = thistype(Timer.expired).first
        local Timer node2 = Timer.getExpired(funcId)
        
        if (node != node2) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE HEAD CORRUPTION")
            set enabled = false
            set node = 1/0
        endif
        
        loop
            if (node != node2) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE CORRUPTION")
                set enabled = false
                set node = 1/0
            endif
        
            set node = node.next
            set node2 = node2.next
            exitwhen node == 0 and node2 == 0
        endloop
    endmethod
    
    private static method expire takes nothing returns boolean
        local Timer node = thistype(Timer.expired).first
        
        if (not enabled) then
            return false
        endif
        
        call validate()
        
        if (count < 100*8) then
            call test()
        endif
        
        loop
            if (LoadInteger(testTable, GetHandleId(testArray[node]), 0) != node) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ERROR: "+I2S(LoadInteger(testTable, GetHandleId(testArray[node]), 0))+ " != "+I2S(node))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Handle Id: "+I2S(GetHandleId(testArray[node])))
                set enabled = false
                return false
            endif
            
            if (GetRandomInt(0, 100) > 75) then
                call DestroyTimer(testArray[node])
                set testArray[node] = null
                call node.destroy()
                set count = count - 1
                call remove(node)
            endif
            if (count < 250*8 and GetRandomInt(0, 100) > 65) then
                call test()
            endif
        
            set node = node.next
            exitwhen node == 0
        endloop
        
        if (count < 100*8) then
            call test()
        endif
        
        call updateFirst(Timer.expired)
        
        return false
    endmethod
    
    private static method onInit takes nothing returns nothing
        set c = Condition(function thistype.expire)
        set m = function thistype.expire
        set funcId = thistype.expire
        call test()
    endmethod
endstruct

struct Tester2 extends array
    private static hashtable testTable = InitHashtable()
    private static timer array testArray
    private static boolexpr c
    private static code m
    private static integer funcId
    
    private static integer count = 0
    
    private static boolean enabled = true
    
    implement TimerHead
    
    private static method test takes nothing returns nothing
        local timer t
        local real timeout = GetRandomReal(.01, 1.5)
        local Timer t2 = Timer.create(timeout, c, funcId)
        if (testArray[t2] != null) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ALLOCATION ERROR")
            set enabled = false
            set timeout = 1/0
        else
            set t = CreateTimer()
        endif
        call TimerStart(t, timeout, true, null)
        call SaveInteger(testTable, GetHandleId(t), 0, t2)
        set testArray[t2] = t
        set t = null
        set count = count + 1
        call add(t2)
    endmethod
    
    private static method validate takes nothing returns nothing
        local Timer node = thistype(Timer.expired).first
        local Timer node2 = Timer.getExpired(funcId)
        
        if (node != node2) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE HEAD CORRUPTION")
            set enabled = false
            set node = 1/0
        endif
        
        loop
            if (node != node2) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE CORRUPTION")
                set enabled = false
                set node = 1/0
            endif
        
            set node = node.next
            set node2 = node2.next
            exitwhen node == 0 and node2 == 0
        endloop
    endmethod
    
    private static method expire takes nothing returns boolean
        local Timer node = thistype(Timer.expired).first
        
        if (not enabled) then
            return false
        endif
        
        call validate()
        
        if (count < 100*8) then
            call test()
        endif
        
        loop
            if (LoadInteger(testTable, GetHandleId(testArray[node]), 0) != node) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ERROR: "+I2S(LoadInteger(testTable, GetHandleId(testArray[node]), 0))+ " != "+I2S(node))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Handle Id: "+I2S(GetHandleId(testArray[node])))
                set enabled = false
                return false
            endif
            
            if (GetRandomInt(0, 100) > 75) then
                call DestroyTimer(testArray[node])
                set testArray[node] = null
                call node.destroy()
                set count = count - 1
                call remove(node)
            endif
            if (count < 250*8 and GetRandomInt(0, 100) > 65) then
                call test()
            endif
        
            set node = node.next
            exitwhen node == 0
        endloop
        
        if (count < 100*8) then
            call test()
        endif
        
        call updateFirst(Timer.expired)
        
        return false
    endmethod
    
    private static method onInit takes nothing returns nothing
        set c = Condition(function thistype.expire)
        set m = function thistype.expire
        set funcId = thistype.expire
        call test()
    endmethod
endstruct

edit
I thought something funny was going on because the fps values were too low on v3. It appeared to be slower than v2... I did a test to see if the timers were running more than they should be, and it turns out that they were

this is bugged at the moment

edit
nvm, I did the test and I'm just getting strange behavior, but no error messages are popping up in the system itself and the runtime variance between the standard timer and the system timer doesn't appear to go past 3.

I really need to investigate this to see wth is going on lol

edit
nvm, had errors in my test code, not the system code

upon fixing test code (stuff I use to test), errors went away

the timeouts weren't the same, which was causing discrepancies in run counts

edit
0 duplicate run-time errors

I now recommend this for use

3.0.0.2 just introduces a slight performance boost as I had an extra useless argument in 2 methods : p. I removed that argument ;D.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
For 3.0.0.3, I improved the creation speed of all timers. I also improved the creation speed for timers that will always merge (small timeouts).

edit
omg o-o

when you use data attachment on native timers, this one wins for 1 shot O_O

63.8 fps (native + hashtable) vs 64 fps (Timer Tools) ;O

This means that WHENEVER you want to do data attachment of any kind, you use Timer Tools.

TimerUtils is now useless... >.<

The only thing native timers are good for now are 1 shot timers with no data attachment or null functions
 
Nes, did I ever tell you how much I love you?

edit
Oh right. That one time.

Either way, wonderful job with TimerTools here :D
What exactly did you do to speed it up? Did you improve creation/destruction, or did you change the algorithm completely? :eek:

edit
Approved.
I used your testcode and it seems to be pretty robust :eek:
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
This is better test code
JASS:
scope Testing
//! runtextmacro TEST_TIMER("1")
//! runtextmacro TEST_TIMER("2")
//! runtextmacro TEST_TIMER("3")
//! runtextmacro TEST_TIMER("4")
//! runtextmacro TEST_TIMER("5")
//! runtextmacro TEST_TIMER("6")

//! textmacro TEST_TIMER takes N
private struct Tester$N$ extends array
    private static hashtable testTable = InitHashtable()
    private static timer array testArray
    private static boolexpr c
    private static code m
    private static integer funcId
    
    private static integer count = 0
    
    private static boolean enabled = true
    private integer ran1
    private integer ran2
    
    implement TimerHead
    
    private static method expire2 takes nothing returns nothing
        local thistype this = LoadInteger(testTable, GetHandleId(GetExpiredTimer()), 0)
        
        set this.ran2 = this.ran2 + 1
        
        if (IAbsBJ(thistype(this).ran1 - thistype(this).ran2) > 1) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"TIMER RUN ERROR #2: "+I2S(thistype(this).ran1)+ " != "+I2S(thistype(this).ran2))
            set enabled = false
        endif
    endmethod
    
    private static method test takes nothing returns nothing
        local timer t
        local real timeout = GetRandomReal(.1, .4)
        local Timer t2 = Timer.create(timeout, c, funcId)
        if (testArray[t2] != null) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ALLOCATION ERROR")
            set enabled = false
            set timeout = 1/0
        else
            set t = CreateTimer()
        endif
        call TimerStart(t, R2I(timeout*320 + .5)/320., true, function thistype.expire2)
        call SaveInteger(testTable, GetHandleId(t), 0, t2)
        set testArray[t2] = t
        set t = null
        set count = count + 1
        call add(t2)
        set thistype(t2).ran1 = 0
        set thistype(t2).ran2 = 0
    endmethod
    
    private static method validate takes nothing returns nothing
        local Timer node = thistype(Timer.expired).first
        local Timer node2 = Timer.getExpired(funcId)
        
        if (node != node2) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE HEAD CORRUPTION")
            set enabled = false
            set node = 1/0
        endif
        
        loop
            if (node != node2) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,I2S(node) + " != "+I2S(node2))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"FUTURE CORRUPTION")
                set enabled = false
                set node = 1/0
            endif
        
            set node = node.next
            set node2 = node2.next
            exitwhen node == 0 and node2 == 0
        endloop
    endmethod
    
    private static method expire takes nothing returns boolean
        local Timer node = thistype(Timer.expired).first
        
        if (not enabled) then
            return false
        endif
        
        call validate()
        
        if (count < 100*2) then
            call test()
        endif
        
        loop
            if (LoadInteger(testTable, GetHandleId(testArray[node]), 0) != node) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"ERROR: "+I2S(LoadInteger(testTable, GetHandleId(testArray[node]), 0))+ " != "+I2S(node))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Handle Id: "+I2S(GetHandleId(testArray[node])))
                set enabled = false
                return false
            endif
            if (GetRandomInt(0, 100*2) > 75) then
                call PauseTimer(testArray[node])
                call DestroyTimer(testArray[node])
                set testArray[node] = null
                call node.destroy()
                set count = count - 1
                call remove(node)
            endif
            if (count < 100*2 and GetRandomInt(0, 100) > 65) then
                call test()
            endif
            
            set thistype(node).ran1 = thistype(node).ran1 + 1
            if (IAbsBJ(thistype(node).ran1 - thistype(node).ran2) > 1) then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"TIMER RUN ERROR: "+I2S(thistype(node).ran1)+ " != "+I2S(thistype(node).ran2))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,R2S(TimerGetRemaining(testArray[node])))
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,R2S(TimerGetTimeout(GetExpiredTimer()))+ " == "+R2S(TimerGetTimeout(testArray[node])))
                set enabled = false
                set node = 1/0
            endif
            set node = node.next
            exitwhen node == 0
        endloop
        
        if (count < 100*2) then
            call test()
        endif
        
        call updateFirst(Timer.expired)
        
        return false
    endmethod
    
    private static method onInit takes nothing returns nothing
        set c = Condition(function thistype.expire)
        set m = function thistype.expire
        set funcId = thistype.expire
        call test()
    endmethod
endstruct
//! endtextmacro
endscope

It does the previous tests + compares how many times the native timer has run to how many times the TimerTool timer has run. If they are more than 1 off, then there is an error.

It performs this comparison from both the native timer and the TimerTool timer.

Also tests both constant merging timers and timers that have chance to merge
 
Level 6
Joined
Oct 23, 2011
Messages
182
JASS:
static method create takes real timeout, boolexpr onExpire, integer funcId returns Timer

What does funcId do??
Some kind of example would be nice, like this

JASS:
    *            Example of Constant Timer Loop 32
    *                struct MyTimer extends array
    *                    integer myValue
    *                    implement CTL
    *                        local string s="My value is "
    *                    implement CTLExpire
    *                        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,s+I2S(myValue))
    *                        call destroy()
    *                    implement CTLNull
    *                        set s=null            //pointless, but shows how to use null block
    *                    implement CTLEnd
    *                endstruct
    *
    *                set MyTimer.create().myValue=16 //will display "My value is 16" 32x a second
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
JASS:
function Hello takes nothing returns boolean
    return false
endfunction

call Timer.create(5, Condition(function Hello), Hello)

edit
for TimerHead, implement that at the top of ur struct. Whenever u add a new timer to struct, call add(timer). Whenever u reomve, call remove(timer) or w/e.

Use this in order to retrieve first timer after doing above

JASS:
*           readonly Timer first
*               -   thistype(Timer.expired).first == Timer.getExpired(funcId)
 
Level 6
Joined
Oct 23, 2011
Messages
182
So something like this for 1-shot timers?

JASS:
struct Test extends array

    private static method onExpr takes nothing returns boolean
        call Timer(Timer.getExpired(thistype.onExpr)).destroy()
        call BJDebugMsg("Hello")
        
        return false
    endmethod
    
    private static method test takes nothing returns boolean
        call Timer(Timer.getExpired(thistype.test)).destroy()
        call Timer.create(5, Condition(function thistype.onExpr), thistype.onExpr)
        
        return false
    endmethod
    
    private static method onInit takes nothing returns nothing
        call Timer.create(5, Condition(function thistype.onExpr), thistype.onExpr)
        call Timer.create(1, Condition(function thistype.test), thistype.test)
    endmethod
endstruct

Is timerhead module for structs only?
 
Level 10
Joined
Sep 19, 2011
Messages
527
So it's supposed to be something like this?

JASS:
struct Foo extends array
    private real bar
    
    private static method expired takes nothing returns boolean
        local Timer t = Timer.getExpired(thistype.expired)
        local thistype this
        
        loop
            exitwhen t == 0
            
            set this = t
            call BJDebugMsg(R2S(this.bar))
            
            // only one shot
            call Timer[t].destroy()
            
            set t = t.next
        endloop
        
        return false
    endmethod
    
    private static method onInit takes nothing returns nothing
        local thistype this = Timer.create(1, Condition(function thistype.expired), thistype.expired)
        
        set this.bar = 4
        
        set this = Timer.create(1, Condition(function thistype.expired), thistype.expired)
        
        set this.bar = 2
    endmethod
endstruct

What does funcId do??

I have the same question.

edit
Also, I cant find how to use TimerHead :\
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
First post now has example with TimerHead

JASS:
implement TimerHead

private Timer timer
private static boolexpr onExpireC
private static integer onExpireI

method destroy takes nothing returns nothing
	//whenever you destroy a timer, remove it
	call remove(timer)
	call timer.destroy()
endmethod

static method create takes nothing returns thistype
	local thistype this = allocate()

	set timer = Timer.create(1, onExpireC, onExpireI)

	//whenever you create a timer, add it
	call add(timer)

	return this
endmethod

private static method onExpire takes nothing returns boolean
	local thistype this = thistype(Timer.expired).first

	loop
		//code
		call destroy()	//destroy the timer?


		//iteration
		set this = Timer(this).next
		exitwhen this == 0
	endloop

	return false
endmethod

//in a module
private static method onInit takes nothing returns nothing
	set onExpireC = Condition(function thistype.onExpire)
	set onExpireI = onExpire
endmethod
 
Level 6
Joined
Oct 23, 2011
Messages
182
There's a really, really weird bug.
Just having the library (I only imported it; didn't do anything yet)
causes random functions to be called (not using timertools)

for instance, this is a part of my spell that i created some time ago

JASS:
        static method moveUnit takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            
            if UnitAlive(GetUnitById(this)) then
                set .tick = .tick - 1
                
                if .tick <= 0 then
                    set .tick = 8
                    
                    call IssuePointOrder(GetUnitById(this), "move", GetUnitX(GetUnitById(this)) + GetRandomReal(-2000, 2000), GetUnitY(GetUnitById(this)) + GetRandomReal(-2000, 2000))
                endif
            else
                call UnitIndex(this).unlock()
                call ReleaseTimer(GetExpiredTimer())
            endif
        endmethod

Whenever I make a new timer and use TimerStart() to callback the function, the timer apparently finishes immediately and calls the function with no data attachment.. only when I have TimerTools library in my map.

is it supposed to conflict with other systems? tbh, I'm too afraid to use this now D=
 
Last edited:
Level 9
Joined
Dec 3, 2010
Messages
162
Is TimerTools at its current state ready to use? Or more precise superior to the 2.x versions with CTC, CTTC, CTM modules or CTL?

Yeah, I'm also interested. Also, was the bug mentioned like 4-5 posts ago fixed?

you could also perhaps update it with your ThrowError functionallity, I think the ThrowError(........) is nicer then DisplayTimedTextToPlayer(...)

Agreed! :p
 
Top