1. Updated Resource Submission Rules: All model & skin resource submissions must now include an in-game screenshot. This is to help speed up the moderation process and to show how the model and/or texture looks like from the in-game camera.
    Dismiss Notice
  2. DID YOU KNOW - That you can unlock new rank icons by posting on the forums or winning contests? Click here to customize your rank or read our User Rank Policy to see a list of ranks that you can unlock. Have you won a contest and still havn't received your rank award? Then please contact the administration.
    Dismiss Notice
  3. We have recently started the 16th edition of the Mini Mapping Contest. The theme is mini RPG. Do check it out and have fun.
    Dismiss Notice
  4. Dismiss Notice
  5. The Highway to Hell has been laid open. Come along and participate in the 5th Special Effect Contest.
    Dismiss Notice
  6. Check out the Staff job openings thread.
    Dismiss Notice
Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

[vJASS] [Snippet] MasterTimer

Discussion in 'Submissions' started by AGD, Sep 30, 2016.

  1. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    This system uses one timer to manage all your timer needs (that means all timeout). It bundles all codes that will expire at the same time using Nestharus' Trigger (This provides some advantages namely, better performance compared to coupling codes using native trigger, and it also allows you to remove a code from a currently executing/expiring timer - which doesn't work correctly when using timer + native trigger).


    Script
    Code (vJASS):

    library MasterTimer /*


        */
    uses /*

        */
    Trigger            /*   https://github.com/nestharus/JASS/blob/master/jass/Systems/Trigger/script.j
        */
    Table              /*   https://www.hiveworkshop.com/threads/snippet-new-table.188084/
        */
    ListT              /*   https://github.com/nestharus/JASS/blob/master/jass/Data%20Structures/ListT/script.j
        */
    StaticUniqueList   /*   https://github.com/nestharus/JASS/blob/master/jass/Data%20Structures/StaticUniqueList/script.j
        */
    ErrorMessage       /*   https://github.com/nestharus/JASS/blob/master/jass/Systems/ErrorMessage/main.j

        */
    //! novjass

        |-------------|
        | Description |
        |-------------|
        /*
            This system uses a single timer to handle all timeouts. It does it by binding all
            the codes that will expire at the same time into a bucket. Each bucket will have
            its own Trigger and a remaining time. The timer runs as a one-shot timer to execute
            the bucket with the least remaining time and then it resets the time of that bucket
            to its timeout/period while the remaining time of the other buckets are updated.
            Then it proceeds to run the next bucket with the least remaining time and this
            process is repeated over again.

        */

        |-----|
        | API |
        |-----|

            struct MasterTimer/*

              */
    static method operator [] takes real timeout returns MasterTimer/*
                - Returns a timeout instance for registration
              */
    method clear takes nothing returns nothing/*
                - Clears all registered codes and Triggers from this timeout

              */
    method register takes code c returns nothing/*
              */
    method unregister takes code c returns nothing/*
                - Registers/Unregisters a code to a timeout instance

              */
    method registerTrigger takes Trigger whichTrigger returns nothing/*
              */
    method unregisterTrigger takes Trigger whichTrigger returns nothing/*
                - Registers/Unregisters a Trigger to a timeout instance


          */
    module MasterTimer/* (Optional)
                - Implement this module inside your struct below your static methods
                  named 'periodic' and 'period'

                Interfaces (Optional):

                  */
    static method period takes nothing returns real/*
                    - Must return the desired periodic method execution timeout
                    - If not found, the timeout would be set to 0.03125 by default
                  */
    static method periodic takes nothing returns nothing/*
                    - The periodic method

                Methods (Provided by the Module):

                  */
    static method startPeriodic takes nothing returns nothing/*
                    - Starts the periodic execution
                  */
    static method stopPeriodic takes nothing returns nothing/*
                    - Stops the periodic execution

        */

        |---------|
        | Example |
        |---------|

            call MasterTimer[0.03125].register(function Periodic)
            call MasterTimer[0.03125].unregister(function Periodic)

        //! endnovjass

        globals
        /*============================================================*/
        /*                    SYSTEM CONFIGURATION                    */
        /*============================================================*/

        /*
            The number of decimal places that will be considered
            for the timeout                                           */

            private constant integer TIMEOUT_PRECISION          = 5
        /*
            When looking for a bucket to register the code, the
            system will check if there is any that will expire at
            the same time as the input code. This value is the
            maximum allowance for the difference in their expiration.
            If the system can't find any existing bucket that meets
            this condition, a new bucket will be created              */

            private constant real EXPIRATION_OFFSET_TOLERANCE   = 0.05

        /*============================================================*/
        /*                    SYSTEM CONFIGURATION                    */
        /*============================================================*/
        endglobals

        private keyword Init

        private struct Bucket extends array

            Trigger trigger
            real timeout
            real remaining
            integer count
            readonly static TableArray bucket
            private static TableArray condition

            implement ListT

            method getBucket takes nothing returns thistype
                local thistype node = this.first
                loop
                    exitwhen node == 0
                    if node.remaining < EXPIRATION_OFFSET_TOLERANCE then
                        set node.count = node.count + 1
                        return node
                    endif
                    set node = node.next
                endloop
                set node = this.enqueue()
                set node.count = 1
                set node.timeout = this.timeout
                set node.remaining = node.timeout
                set node.trigger = Trigger.create(false)
                return node
            endmethod

            method removeFromBucket takes integer index returns nothing
                local thistype node = bucket[this][index]
                call bucket[this].remove(index)
                call condition[this].remove(index)
                set node.count = node.count - 1
                if node.count == 0 then
                    call node.remove()
                    call node.trigger.destroy()
                endif
            endmethod

            method register takes boolexpr expr returns nothing
                local thistype node = this.getBucket()
                local integer id = GetHandleId(expr)
                set bucket[this][id] = node
                set condition[this][id] = node.trigger.register(expr)
            endmethod

            method unregister takes boolexpr expr returns nothing
                local integer id = GetHandleId(expr)
                call TriggerCondition(condition[this][id]).destroy()
                call this.removeFromBucket(id)
            endmethod

            method registerTrigger takes Trigger trig returns nothing
                local thistype node = this.getBucket()
                set bucket[this][trig] = node
                set condition[this][trig] = node.trigger.reference(trig)
            endmethod

            method unregisterTrigger takes Trigger trig returns nothing
                call TriggerReference(condition[this][trig]).destroy()
                call this.removeFromBucket(trig)
            endmethod

            private static method init takes nothing returns nothing
                set bucket = TableArray[0x2000]
                set condition = TableArray[0x2000]
            endmethod
            implement Init

        endstruct

        private struct PeriodList extends array

            private static timer timer = CreateTimer()

            implement StaticUniqueList

            private static method periodic takes nothing returns nothing
                local real timeout = TimerGetTimeout(timer)
                local real leastRemaining = 0.00
                local thistype this = first
                local Bucket node
                loop
                    exitwhen this == sentinel
                    set node = Bucket(this).first
                    loop
                        exitwhen node == 0
                        set node.remaining = node.remaining - timeout
                        if node.remaining == 0.00 then
                            set node.remaining = node.timeout
                            call node.trigger.fire()
                        endif
                        if leastRemaining == 0.00 or node.remaining < leastRemaining then
                            set leastRemaining = node.remaining
                        endif
                        set node = node.next
                    endloop
                    set this = this.next
                endloop
                call TimerStart(timer, leastRemaining, false, function thistype.periodic)
            endmethod

            method register takes nothing returns nothing
                if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                    call enqueue(this)
                    if first == this or Bucket(this).timeout < TimerGetRemaining(timer) then
                        call TimerStart(timer, Bucket(this).timeout, false, function thistype.periodic)
                    endif
                endif
            endmethod

            method unregister takes nothing returns nothing
                if Bucket(this).first == 0 then
                    call this.remove()
                    if first == sentinel then
                        call PauseTimer(timer)
                    endif
                endif
            endmethod

        endstruct

        struct MasterTimer extends array

            debug private static TableArray registered
            private static Table bucket
            private static real factor

            static method operator [] takes real timeout returns thistype
                local integer id = R2I(timeout*factor)
                local Bucket this = bucket[id]
                if this == 0 then
                    set this = Bucket.create()
                    set this.timeout = timeout
                    set bucket[id] = this
                endif
                return this
            endmethod

            method register takes code c returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "register()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(c == null, "MasterTimer", "register()", "thistype", this, "Attempted to register a null code")
                debug call ThrowError(registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "register()", "thistype", this, "Attempted to register an already registered code")
                debug set registered[this].boolean[GetHandleId(Filter(c))] = true
                call Bucket(this).register(Filter(c))
                call PeriodList(this).register()
            endmethod

            method unregister takes code c returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "unregister()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(c == null, "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister a null code")
                debug call ThrowError(not registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister an unregistered code")
                debug call registered[this].boolean.remove(GetHandleId(Filter(c)))
                call Bucket(this).unregister(Filter(c))
                call PeriodList(this).unregister()
            endmethod

            method registerTrigger takes Trigger trig returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "registerTrigger()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(trig == 0, "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register a null Trigger")
                debug call ThrowError(registered[this].boolean[trig], "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register an already registered Trigger")
                debug set registered[this].boolean[trig] = true
                call Bucket(this).registerTrigger(trig)
                call PeriodList(this).register()
            endmethod

            method unregisterTrigger takes Trigger trig returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "unregisterTrigger()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(trig == 0, "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister a null Trigger")
                debug call ThrowError(not registered[this].boolean[trig], "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister an unregistered Trigger")
                debug call registered[this].boolean.remove(trig)
                call Bucket(this).unregisterTrigger(trig)
                call PeriodList(this).unregister()
            endmethod

            method clear takes nothing returns nothing
                local Bucket node = Bucket(this).first
                debug call ThrowWarning(this == node, "MasterTimer", "clear()", "thistype", this, "Attempted to clear an empty instance")
                debug call ThrowError(this == 0, "MasterTimer", "clear()", "thistype", 0, "Attempted to clear a null instance")
                loop
                    exitwhen node == 0
                    set node.count = 0
                    call node.trigger.destroy()
                    call node.remove()
                    set node = node.next
                endloop
                call PeriodList(this).unregister()
                call Bucket(this).destroy()
                call bucket.remove(R2I(Bucket(this).timeout*factor))
                call Bucket.bucket[this].flush()
                debug call registered[this].flush()
            endmethod

            private static method init takes nothing returns nothing
                debug set registered = TableArray[0x2000]
                set bucket = Table.create()
                set factor = Pow(10, TIMEOUT_PRECISION)
            endmethod
            implement Init

        endstruct

        private module Init
            private static method onInit takes nothing returns nothing
                call init()
            endmethod
        endmodule

        module MasterTimer

            static if thistype.periodic.exists then
                static method startPeriodic takes nothing returns nothing
                    static if thistype.period.exists then
                        call MasterTimer[period()].register(function thistype.periodic)
                    else
                        call MasterTimer[0.031250000].register(function thistype.periodic)
                    endif
                endmethod

                static method stopPeriodic takes nothing returns nothing
                    static if thistype.period.exists then
                        call MasterTimer[period()].unregister(function thistype.periodic)
                    else
                        call MasterTimer[0.031250000].unregister(function thistype.periodic)
                    endif
                endmethod
            endif

        endmodule


    endlibrary
     



    Changelog

    v1.0
    - First Release
     
    Last edited: Nov 12, 2017
  2. Aniki

    Aniki

    Joined:
    Nov 7, 2014
    Messages:
    524
    Resources:
    4
    Spells:
    1
    JASS:
    3
    Resources:
    4
    Not sure why you switched to using function interfaces (
    function interface LoopCode takes nothing returns nothing
    ) from using code, because function interfaces result in code duplication and trigger evaluations.

    I don't know why you are not using structs (for readability) and I still don't know why you want to be so stingy with using timers/handles.

    It seems to me that something like this is both easier to write and [arguably] use:
    Code (vJASS):

    library DelayedCountedExec

    struct DelayedCountedExec
        static thistype instance
        static constant integer NO_COUNT = -1

        integer data // user data
        real delay
        integer eval_count = 0
        integer max_eval_count

        public boolexpr be
        public triggercondition tc
        public trigger trg
        public timer tmr

        method destroy takes nothing returns nothing
            call TimerStart(this.tmr, 0.0, false, null)
            call TriggerRemoveCondition(this.trg, this.tc)
            call DestroyBoolExpr(this.be)
            call this.deallocate()
        endmethod

        private static method dispatch takes nothing returns nothing
            local thistype this = R2I(TimerGetRemaining(GetExpiredTimer()) + 0.5)

            set this.eval_count = eval_count + 1
            set instance = this
            call TriggerEvaluate(this.trg)

            if this.max_eval_count != NO_COUNT and this.eval_count >= this.max_eval_count then
                call this.destroy()
            else
                call TimerStart(this.tmr, this, false, null)
                call PauseTimer(this.tmr)
                call TimerStart(this.tmr, this.delay, false, function thistype.dispatch)
            endif
        endmethod

        static method create takes integer data, real delay, integer count, code callback returns thistype
            local thistype this = allocate()

            set this.data = data
            set this.delay = delay
            set this.max_eval_count = count

            if this.trg == null then
                set this.trg = CreateTrigger()
            endif
            if this.tmr == null then
                set this.tmr = CreateTimer()
            endif

            set this.be = Condition(callback)
            set this.tc = TriggerAddCondition(this.trg, this.be)

            // could use a hashtable but meh...
            call TimerStart(this.tmr, this, false, null)
            call PauseTimer(this.tmr)
            call TimerStart(this.tmr, this.delay, false, function thistype.dispatch)

            return this
        endmethod
    endstruct

    endlibrary
     


    silly example usage:
    Code (vJASS):

    function create_footmen takes nothing returns boolean
        local DelayedCountedExec de = DelayedCountedExec.instance
        local integer i

        set i = 1
        loop
            exitwhen i > de.eval_count
            call CreateUnit(Player(de.data), 'hfoo', 0.0, 0.0, 270.0)
            set i = i + 1
        endloop

        set de.delay = de.delay + 1.0

        if de.eval_count == de.max_eval_count then
            call BJDebugMsg("no more footmen!")
        endif

        return false
    endfunction

    globals
        DelayedCountedExec footmen_de
    endglobals
    function use_delayed_counted_exec takes nothing returns nothing
        set footmen_de = DelayedCountedExec.create(0, 1.0, 3, function create_footmen)
    endfunction
     
     
  3. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    With using code, I'm forced to save the data using Table/hashtable while with function interfaces, I can easily use the function as index for arrays. Although there is that downside you mentioned, it would not matter in-game and it would even be slightly faster than using code.

    Because if it could be minimized, why not?


    Btw, aren't struct members public by default?
     
  4. Aniki

    Aniki

    Joined:
    Nov 7, 2014
    Messages:
    524
    Resources:
    4
    Spells:
    1
    JASS:
    3
    Resources:
    4
    You are trading execution time for RAM and I don't think that's a good trade-off. Because the amount of RAM used is kind of negligible and there's plenty of it, on the other hand the Jass VM is not exactly fast, it doesn't take that much operations for it to start lowering the frame rate and/or freeze the game. So I think the operations that LoopCode would take can be better spent someplace else.

    I think they are, but when I was doing some tests I changed the visibility from private to public and forgot to set it back to private, these:
    Code (vJASS):

        public boolexpr be
        public triggercondition tc
        public trigger trg
        public timer tmr
     

    were meant to be private.
     
  5. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,226
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
    function RegisterLoopCode takes LoopCode c, real timeout returns boolean

    Can be achieved very straight forward with single timer by user.

    Code (vJASS):
    function RegisterLoopCodeCounted takes LoopCode c, real timeout, integer executionCount returns boolean
    function RegisterLoopCodeTimed takes LoopCode c, real timeout, real duration returns boolean

    Those may be useful, but are a bit restricted in vJASS, I believe. For example I can't really use them for my struct instances, as I can't assosiate my instance in the callback, meaning I have to loop through all instances in the callback, meaning they are foreced to share the same remaining time/counter. If I want each registred instance to run X times / or Y seconds, it won't work.

    And as Aniki questioned, interface is maybe a lose here.

    Maybe trigger conditions + possibility to bind an integer (struct instance) would be a good soluition? : )
     
  6. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    Atm, I'm thinking of having this graveyarded. I think this implementation of 1 timer for all different timeout is not really a good idea. This will run 32 FPS even when the registered codes all have low frequency. Plus, when a user wants a really high frequency execution, it would not run evenly which would maybe cause some unexpected problems.
     
  7. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    I was able to make a new design that still uses a single timer for all timeouts but unlike this one, it runs evenly for all timeouts. It does not have a minimum timeout limit and it does not run 33 fps regardless of the frequency of the registered codes' timeout. However, I'm not good in benchmarks and stress tests so I don't know the difference in performance between this, other timer systems, and with native timers =).


    Here's the new design
    Code (vJASS):

    library MasterTimer /*


        */
    uses /*

        */
    Trigger            /*
        */
    Table              /*
        */
    ListT              /*
        */
    StaticUniqueList   /*
        */
    ErrorMessage       /*

        */
    //! novjass

        |-------------|
        | Description |
        |-------------|
        /*
            This system uses a single timer to handle all timeouts. It does it by binding all
            the codes that will expire at the same time into a bucket. Each bucket will have
            its own Trigger and a remaining time. The timer runs as a one-shot timer to execute
            the bucket with the least remaining time and then it resets the time of the bucket
            to its timeout/period and then it proceeds to run the next bucket with the least
            remaining time and this process is repeated over again.

        */

        |-----|
        | API |
        |-----|

            struct MasterTimer/*

              */
    static method operator [] takes real timeout returns MasterTimer/*
              */
    method clear takes nothing returns nothing/*
              */
    method register takes code c returns nothing/*
              */
    method unregister takes code c returns nothing/*
              */
    method registerTrigger takes Trigger whichTrigger returns nothing/*
              */
    method unregisterTrigger takes Trigger whichTrigger returns nothing/*


          */
    module MasterTimer/*

        */

        |---------|
        | Example |
        |---------|

            call MasterTimer[0.03125].register(function Periodic)
            call MasterTimer[0.03125].unregister(function Periodic)

        //! endnovjass

        globals
            private constant integer TIMEOUT_PRECISION = 5
            private constant real TIMEOUT_OFFSET_TOLERANCE = 0.05
        endglobals

        private keyword Init

        private struct Bucket extends array

            Trigger trigger
            real timeout
            real remaining
            integer count
            readonly static TableArray bucket
            private static TableArray condition

            implement ListT

            method getBucket takes nothing returns thistype
                local thistype node = this.first
                loop
                    exitwhen node == 0
                    if node.remaining < TIMEOUT_OFFSET_TOLERANCE then
                        set node.count = node.count + 1
                        return node
                    endif
                    set node = node.next
                endloop
                set node = this.enqueue()
                set node.count = 1
                set node.timeout = this.timeout
                set node.remaining = node.timeout
                set node.trigger = Trigger.create(false)
                return node
            endmethod

            method removeFromBucket takes integer index returns nothing
                local thistype node = bucket[this][index]
                call bucket[this].remove(index)
                call condition[this].remove(index)
                set node.count = node.count - 1
                if node.count == 0 then
                    call node.remove()
                    call node.trigger.destroy()
                endif
            endmethod

            method register takes boolexpr expr returns nothing
                local thistype node = this.getBucket()
                local integer id = GetHandleId(expr)
                set bucket[this][id] = node
                set condition[this][id] = node.trigger.register(expr)
            endmethod

            method unregister takes boolexpr expr returns nothing
                local integer id = GetHandleId(expr)
                call TriggerCondition(condition[this][id]).destroy()
                call this.removeFromBucket(id)
            endmethod

            method registerTrigger takes Trigger trig returns nothing
                local thistype node = this.getBucket()
                set bucket[this][trig] = node
                set condition[this][trig] = node.trigger.reference(trig)
            endmethod

            method unregisterTrigger takes Trigger trig returns nothing
                call TriggerReference(condition[this][trig]).destroy()
                call this.removeFromBucket(trig)
            endmethod

            private static method init takes nothing returns nothing
                set bucket = TableArray[0x2000]
                set condition = TableArray[0x2000]
            endmethod
            implement Init

        endstruct

        private struct PeriodList extends array

            private static timer timer = CreateTimer()

            implement StaticUniqueList

            private static method leastRemaining takes nothing returns real
                local thistype this = first
                local Bucket node
                local real least = -1.00
                loop
                    exitwhen this == sentinel
                    set node = Bucket(this).first
                    loop
                        exitwhen node == 0
                        if least < 0.00 or node.remaining < least then
                            set least = node.remaining
                        endif
                        set node = node.next
                    endloop
                    set this = this.next
                endloop
                return least
            endmethod

            private static method periodic takes nothing returns nothing
                local real timeout = TimerGetTimeout(timer)
                local real nextTimeout = 0.00
                local thistype this = first
                local Bucket node
                loop
                    exitwhen this == sentinel
                    set node = Bucket(this).first
                    loop
                        exitwhen node == 0
                        set node.remaining = node.remaining - timeout
                        if node.remaining == 0.00 then
                            set node.remaining = node.timeout
                            call node.trigger.fire()
                        endif
                        if nextTimeout == 0.00 or node.remaining < nextTimeout then
                            set nextTimeout = node.remaining
                        endif
                        set node = node.next
                    endloop
                    set this = this.next
                endloop
                call TimerStart(timer, nextTimeout, false, function thistype.periodic)
            endmethod

            method register takes nothing returns nothing
                if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                    call enqueue(this)
                    if first == this then
                        call TimerStart(timer, leastRemaining(), false, function thistype.periodic)
                    endif
                endif
            endmethod

            method unregister takes nothing returns nothing
                if Bucket(this).first == 0 then
                    call this.remove()
                    if first == sentinel then
                        call PauseTimer(timer)
                    endif
                endif
            endmethod

        endstruct

        struct MasterTimer extends array

            debug private static TableArray registered
            private static Table bucket
            private static real factor

            static method operator [] takes real timeout returns thistype
                local integer id = R2I(timeout*factor)
                local Bucket this = bucket[id]
                if this == 0 then
                    set this = Bucket.create()
                    set this.timeout = timeout
                    set bucket[id] = this
                endif
                return this
            endmethod

            method register takes code c returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "register()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(c == null, "MasterTimer", "register()", "thistype", this, "Attempted to register a null code")
                debug call ThrowError(registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "register()", "thistype", this, "Attempted to register an already registered code")
                debug set registered[this].boolean[GetHandleId(Filter(c))] = true
                call Bucket(this).register(Filter(c))
                call PeriodList(this).register()
            endmethod

            method unregister takes code c returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "unregister()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(c == null, "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister a null code")
                debug call ThrowError(not registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister an unregistered code")
                debug call registered[this].boolean.remove(GetHandleId(Filter(c)))
                call Bucket(this).unregister(Filter(c))
                call PeriodList(this).unregister()
            endmethod

            method registerTrigger takes Trigger trig returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "registerTrigger()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(trig == 0, "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register a null Trigger")
                debug call ThrowError(registered[this].boolean[trig], "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register an already registered Trigger")
                debug set registered[this].boolean[trig] = true
                call Bucket(this).registerTrigger(trig)
                call PeriodList(this).register()
            endmethod

            method unregisterTrigger takes Trigger trig returns nothing
                debug call ThrowError(this == 0, "MasterTimer", "unregisterTrigger()", "thistype", 0, "Attempted to use a null instance")
                debug call ThrowError(trig == 0, "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister a null Trigger")
                debug call ThrowError(not registered[this].boolean[trig], "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister an unregistered Trigger")
                debug call registered[this].boolean.remove(trig)
                call Bucket(this).unregisterTrigger(trig)
                call PeriodList(this).unregister()
            endmethod

            method clear takes nothing returns nothing
                local Bucket node = Bucket(this).first
                debug call ThrowError(this == node, "MasterTimer", "clear()", "thistype", this, "Attempted to clear an empty instance")
                debug call ThrowError(this == 0, "MasterTimer", "clear()", "thistype", 0, "Attempted to clear a null instance")
                loop
                    exitwhen node == 0
                    set node.count = 0
                    call node.trigger.destroy()
                    call node.remove()
                    set node = node.next
                endloop
                call Bucket(this).destroy()
                call bucket[this].remove(R2I(Bucket(this).timeout*factor))
                call Bucket.bucket[this].flush()
                debug call registered[this].flush()
            endmethod

            private static method init takes nothing returns nothing
                debug set registered = TableArray[0x2000]
                set bucket = Table.create()
                set factor = Pow(10, TIMEOUT_PRECISION)
            endmethod
            implement Init

        endstruct

        private module Init
            private static method onInit takes nothing returns nothing
                call init()
            endmethod
        endmodule

        module MasterTimer

            static if thistype.periodic.exists then
                static method startPeriodic takes nothing returns nothing
                    static if thistype.period.exists then
                        call MasterTimer[period()].register(function thistype.periodic)
                    else
                        call MasterTimer[0.031250000].register(function thistype.periodic)
                    endif
                endmethod

                static method stopPeriodic takes nothing returns nothing
                    static if thistype.period.exists then
                        call MasterTimer[period()].unregister(function thistype.periodic)
                    else
                        call MasterTimer[0.031250000].unregister(function thistype.periodic)
                    endif
                endmethod
            endif

        endmodule


    endlibrary
     
    I'm still not done with the documentation.


    My only problem with this is that I don't know of a way to get the handle of the executing triggercondition/boolexpr from inside the executing code in the same manner as GetTriggeringTrigger().
     
  8. Almia

    Almia

    Joined:
    Apr 24, 2012
    Messages:
    4,843
    Resources:
    35
    Spells:
    30
    Tutorials:
    4
    JASS:
    1
    Resources:
    35
    sorry TL;DR

    is this library 1:M timer?
     
  9. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    The system uses 1 timer for all codes
     
  10. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,226
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
    AGD, is it planned to finish the update / update first post? : )
     
  11. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    Sorry, I was away for like a month because of so many things. Anyway, I'll be updating this soon (maybe a bit later because even now I don't have much free time). I also found an error in my algorithm which will be fixed on the next update.
     
  12. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    Update finished. First post updated.
     
  13. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,226
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
    Just a thought. If a bucket expires/gets fired, a check for a potential bucket combination could be applied, as in theory they could be in allowed epsilon/tollerance after the run, while they were not before with current check.

    Code (vJASS):

            private static method init takes nothing returns nothing
                set bucket = TableArray[0x2000]
                set condition = TableArray[0x2000]
            endmethod
            implement Init

    Maybe
    implement InitTimer
    would be cool, because always so many operations onInits probably should be tried to be avoided.

    Code (vJASS):
    if node.remaining == 0.00 then
       set node.remaining = node.timeout
       call node.trigger.fire()
    endif

    Why
    ==
    instead of
    <=
    ?
    forget it

    Code (vJASS):

            method register takes nothing returns nothing
                if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                    call enqueue(this)
                    if first == this or Bucket(this).timeout < TimerGetRemaining(timer) then
                        call TimerStart(timer, Bucket(this).timeout, false, function thistype.periodic)
                    endif
                endif
            endmethod
     

    The first check, ehm, means
     if (Bucket(this).first != 0 or Bucket(this).last != 0)

    Can the last be not "0", while first is "0", or other way around? Why the check?

    Sometimes ifs compare with
     0
    , sometimes with
    sentinel
    , while it's sematically the same, is there a reason?

    Code (vJASS):

    loop
        exitwhen node == 0
        set node.count = 0
        call node.trigger.destroy()
        call node.remove()
        set node = node.next
    endloop

    Are you sure this works? I haven't tried it with a demo, and didn't check the list logics, but I might imagine this would be required:

    Code (vJASS):

    loop
        exitwhen node == 0
        temp = node.next
        set node.count = 0
        call node.trigger.destroy()
        call node.remove()
        set node = temp
    endloop


    Why not letting assosciate an integer with registration to allow struct binding? Something alike is probably very important.
     
    Last edited: Dec 22, 2017
  14. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    Right, I'll make it configurable next update.


    Ahh, you mean the two TableArray allocations? They actually are O(1) operations despite the sizes specified. Creating 1 TableArray of size 0x2000 is just about as light as creating 1 Table. It's one reason why I prefer using TableArrays instead of having array of Tables (instance member Tables) which needed to be dynamically created/destroyed along with struct object creation/destruction because the former only needs to be created once (at init) and accessing the Table from a TableArray is just a simple arithmetic (addition; Table instance + index) operation.


    if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then

    What I'm actually trying to do here is to check if there is only one node inside the
    Bucket(this)
    list. To do it I imagine there are two cases where the first and last node is equal. The first case is if the list is empty (first and last node is 0) and the second case is if there is only one node in the list (first and last node is non-zero but equal). The first expression
    Bucket(this).first != 0
    eliminates the first case (ensures that the list not empty) while the second expression
    Bucket(this).last == Bucket(this).first
    checks if it satisfies the other case, which ensures that there is only one node inside the list.


    Ah that's because
    sentinel
    is only found on the StaticUniqueList module and not on ListT module so I decided that only when dealing with StaticUniqueList should I use it, but yeah I can make it uniform by using 0 in both.


    Alright, I forgot about these because I was accustomed to using my own lists which doesn't break the prev & next links of the node even when it is removed. I'll fix it.


    Still can't find a way to do these because I don't know where to attach my integer. I can't attach it to the timer cause there is only one. I can't also attach it to the Trigger since one trigger contains many codes. It should be attached to the triggercondition but then I have no way of accessing the current executing triggercondition inside the executing code =(. But I was thinking it's still preferable for users to loop through all there instances (as what is usually done with a single timer for all spell instances) with the difference that if you use this system, you don't need to have 1 Timer : 1 Spell but rather, all spell with the same timeout will have a chance of being bundled together into 1 trigger (only one trigger evaluation compared to many timer expiration, as what CTL for example do for similar cases, although in CTL the chance of them being bundled is 100%).
     
  15. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,226
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
    Maybe it doesn't even make sense realistically, if implementation makes mit unnecessarily more complex in a way.. was just thinking loud.^^

    List seems to have:
    Code (vJASS):

    static method operator sentinel takes nothing returns integer
    return 0
    endmethod

    but yeh, dosn't really matter.
    Ah, ok. I somehow didn't realize that.

    But attaching data to [Bucket][Node] could work, or?
     
  16. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    But neither Bucket (list) nor the node instance is unique for every registered code. A node is unique for every Trigger (bucket) though - which contains many codes.
     
  17. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,226
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
    But nodes are enqueued to the bucket list with new registration. Why is the combination bucket and node not unique?
    Hm, but a trigger is created for each node? I'm confused you say Trigger = Bucket.
    Code (vJASS):
    set node.trigger = Trigger.create(false)

    What if I have timeout 1 second for all my instances. Why would I want to loop through all my instances each time when I need to run only my expiring one. Getting struct instance seems like much bigger win for such system than reducing timer usage. ;s
     
  18. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    400
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    I mean yes, the combination for [bucket][node] corresponds to a unique trigger, but not to a unique code since a that trigger contains many codes that will expire at the same time. In fact node in itself is already unique since no bucket contains the same nodes.

    My bad, I meant trigger == node

    Yeah, this is quite important if you want to be able to do things like when using TimerUtils and you want to avoid additional data structures. However, for this to work, I first need to be able to solve the first problem (discussed above), and then be able to add the same code more than once to the same timeout (which currently doesn't work).
    Maybe I can find a way soon.
     
  19. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    8,121
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    I did a Timer system called T2 (which is graveyarded due to it not being worth it) which also used one timer.

    The benchmark results are: use a timer with a hashtable, a la TimerUtilsEx. For those fast ones, use Timer32.
     
  20. Nestharus

    Nestharus

    Joined:
    Jul 10, 2007
    Messages:
    6,146
    Resources:
    8
    Spells:
    3
    Tutorials:
    4
    JASS:
    1
    Resources:
    8
    Just going to throw out some thoughts here from when I was working on timers.

    I had personally found that a single timer system wasn't feasible from a performance perspective ; ). What I ended up doing was creating 1 timer for each timeout. For a given timeout, iterate over a list as the order will always be the same ^_^. Now there is only 1 GetHandleId call/hashtable read for a given timeout (or do some math, whatever ends up being faster) and then N array reads. Fewer timers and lighter calls. Simplifies the code as well.

    In the case of a bucket per timeout, I would argue against the use of Trigger. Timer code is going to be changing a lot. It is always in flux. Trigger will simplify removing conditions on an executing native trigger, but the cost is that it's going to have a lot of overhead in terms of creation/destruction and adding/removing. Timers add/remove a lot. If you are going for performance, it doesn't exactly make sense to use Trigger = ). I think in my little Timer lib, I ended up going for native triggers. I just removed the code after the trigger execution or something. If I called the trigger explicitly, it was in the cleanup of the caller method. If it was all on one trigger, it was on the next cycle.

    Using one timer for everything is a cool and novel idea. I just don't think that it's particularly useful ^_^. It's more of an exercise in thought in my opinion : ).