• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Timeout

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Timeout is a new vJass library for handling timer callbacks with as little nonsense as possible. Using Table version 6, common WarCraft 3 timer API is handled behind-the-scenes, and you can just focus on getting your code from A to B.

When running in TEST mode, it can provide instructive debugging messages.

Basic Example (shows how short the code can be):
JASS:
function DestroyFX takes nothing returns nothing
    call DestroyEffect(Timeout.getData().effect[1])
endfunction

function AddFXTimed takes effect whichEffect, real duration returns Table
    return Timeout.start(duration, false, function DestroyFX).effect.save(1, whichEffect)
endfunction

Core API:
JASS:
Timeout.start(
  real duration // how long the timeout should last for
  boolean isInterval // whether to repeat indefinitely (true) or expire only once (false)
  code callbackFn // what function to run after the timeout expires
) -> Table
Creates a new Table for you to pack some data into, rather than you passing data to it.
All keys but the handle ID of the timer (integer), as well as the integer and boolean on index -1 are available to use. The Table will be garbage collected when the timer is finished.

JASS:
local Table data = Timeout.getData()
You'd access this static property from a callback function, and it points to the Table that holds your data. Accessing this property will automatically cleanup the expired timer for a non-repeating timer.

JASS:
local Table data = Timeout.complete()
This is a function similar to getData, except that it will clean up the expired timer for intervals as well.


JASS:
local Table data = Timeout[GetExpiredTimer()]
The static method operator [] on the Timeout struct converts a timer into the Table stored on it. It is similar to Timeout.getData()/Timeout, and is only meant to be used in cases where you intend to resume an expired one-shot timer.

JASS:
local Table data = Timeout.restart(timer, duration, isInterval, callbackFn)
This is like Timeout.start, but the first argument is the timer we're trying to restart. This method is intend to be used when the data on the timer needs to be preserved, and you'd rather not go through the trouble of copying data from one Table over to the fresh Table that would otherwise be provided by Timeout.start.

JASS:
local timer t = Timeout.getTimer(data)
If you want access to the timer stored inside of the Table.


The Code:
JASS:
// Timeout version 1.0 by Bribe
// Hosted at: https://www.hiveworkshop.com/threads/timeout.351618/
// Requires Table version 6: https://www.hiveworkshop.com/threads/188084/
library Timeout requires Table
    globals
        private constant boolean TEST = true
    endglobals
    struct Timeout extends array // "extends" Table

        static if TEST then
            private static boolean disableUnknownError = false
        endif

        static method operator [] takes timer obj returns Table
            static if TEST then
                local Table data = Table(thistype.typeid).get(obj)
                if obj == null then
                    call BJDebugMsg("Timeout[null timer] Error!")
                elseif data == 0 then
                    if thistype.disableUnknownError then
                        set thistype.disableUnknownError = false
                    else
                        call BJDebugMsg("Timeout[unknown timer] Error!")
                    endif
                endif
                return data
            endif
            return Table(thistype.typeid).get(obj)
        endmethod

        static method getTimer takes Table data returns timer
            static if TEST then
                local timer obj = data.timer[-1]
                if obj == null then
                    call BJDebugMsg("Timeout.getTimer(Table(" + I2S(data) + ")) -> null Error!")
                endif
                return obj
            else
                return data.timer[-1]
            endif
        endmethod

        static method restart takes timer obj, real duration, boolean isInterval, code callbackFn returns Table
            local Table data = Timeout[obj]
            static if TEST then
                local string recommendation
            endif
            if obj == null then
                return 0
            elseif data == 0 then
                set data = Table(thistype.typeid).bind(obj)
            else
                static if TEST then
                    if not data.handle.has(-1) then
                        if data.boolean[-1] then
                            set recommendation = "Use Timeout.stop(Table, false) rather than Timeout.complete() when you don't want to destroy a repeating timer."
                        else
                            set recommendation = "Use Timeout[GetExpiredTimer()] rather than Timeout.getData() when you don't want to destroy a one-shot timer."
                        endif
                        call BJDebugMsg("Timer.restart(zombie timer) -> Table(" + I2S(data) + ") Warning: " + recommendation)
                    endif
                endif
            endif
            set data.timer[-1] = obj
            set data.boolean[-1] = isInterval
            call TimerStart(obj, duration, isInterval, callbackFn)
            return data
        endmethod

        static method start takes real duration, boolean isInterval, code callbackFn returns Table
            static if TEST then
                set thistype.disableUnknownError = true
            endif
            return Timeout.restart(CreateTimer(), duration, isInterval, callbackFn)
        endmethod

        private static method asyncSelfDestruct takes nothing returns nothing
            local timer obj = GetExpiredTimer()
            call Table(thistype.typeid).forget(obj)
            call DestroyTimer(obj)
            set obj = null
        endmethod

        static method stop takes Table data, boolean destroyTimer returns Table
            local timer obj = Timeout.getTimer(data)
            if obj == null then
                return 0
            endif
            call PauseTimer(obj)
            if destroyTimer then
                call data.handle.remove(-1)
                call TimerStart(obj, 0, false, function thistype.asyncSelfDestruct)
            endif
            set obj = null
            return data
        endmethod

        static method getData takes nothing returns Table
            local Table data = Timeout[GetExpiredTimer()]
            local boolean isInterval = data.boolean[-1]
            if not isInterval then
                call Timeout.stop(data, true)
            endif
            return data
        endmethod

        static method complete takes nothing returns Table
            return Timeout.stop(Timeout[GetExpiredTimer()], true)
        endmethod
    endstruct
endlibrary


JASS:
library TimeoutTests initializer Init requires Timeout
    globals
        private constant integer BAD_INTERVAL_CALL_EXAMPLE = 1
        private constant integer GOOD_INTERVAL_CALL_EXAMPLE = 2
        private integer testPhase = BAD_INTERVAL_CALL_EXAMPLE
    endglobals

    private function Callback takes nothing returns nothing
        local Table data = Timeout.getData()
        call BJDebugMsg("Table " + I2S(data) + data.string[1])
    endfunction

    private function StandardInterval takes nothing returns nothing
        local Table data = Timeout.getData()
        local integer count = data[1]
        local boolean badExample = testPhase == BAD_INTERVAL_CALL_EXAMPLE

        call BJDebugMsg("Table " + I2S(data) + " has " + I2S(count) + " seconds remaining.")

        if badExample then
            if count < -2 then
                call BJDebugMsg("Forgetting to call `complete` or `stop` will cause the interval to continue on forever. Let's try again.")
                set testPhase = GOOD_INTERVAL_CALL_EXAMPLE
                set data[1] = 3
                return
            endif
        elseif count == 0 then
            call Timeout.stop(data, true)
            call BJDebugMsg("Tests have concluded.")
        endif
        set count = count - 1
        set data[1] = count
    endfunction

    private function IrregularInterval takes nothing returns nothing
        local Table data
        local integer count
        local boolean badExample = testPhase == BAD_INTERVAL_CALL_EXAMPLE
        if badExample then
            set data = Timeout.getData()
        else
            set data = Timeout[GetExpiredTimer()]
        endif
        set count = data[1]
        call BJDebugMsg("Table " + I2S(data) + " has " + I2S(count) + " seconds remaining.")
        if count == 0 then
            if badExample then
                call BJDebugMsg("Timeout should no longer show warnings in TEST mode. Let's try again with the non-destructive Timeout[GetExpiredTimer()] call.")
                set testPhase = GOOD_INTERVAL_CALL_EXAMPLE
                call Timeout.start(1, false, function IrregularInterval).save(1, 3)
            else
                call Timeout.complete()
                set testPhase = BAD_INTERVAL_CALL_EXAMPLE
                call BJDebugMsg("Now show an example where we forget to complete a repeating timer.")
                call Timeout.start(1, true, function StandardInterval).save(1, 3)
            endif
            return
        elseif count == 3 and badExample then
            call BJDebugMsg("Timeout should show warnings in TEST mode because we are going to test cases where we restart an expired one-shot timer.")
        endif
        call Timeout.restart(GetExpiredTimer(), 1, false, function IrregularInterval)
        set count = count - 1
        set data[1] = count
    endfunction
    private function Init takes nothing returns nothing
        call Timeout.start(1, false, function Callback).string.save(1, " has been called after 1 second")
        call Timeout.start(2, false, function Callback).string.save(1, " has been called after 2 seconds")
        call Timeout.start(3, false, function IrregularInterval).save(1, 3)
    endfunction
endlibrary
 
Last edited:
Woah, talk about a new age of vJASS resources now made possible with Table 6. That said, I wanted to ask the following about the resource itself:

  • I noticed numerous direct accesses to the Table object Table(thistype.typeid). Wouldn't the allocation schema of Table objects put the aforementioned object above at risk of being overwritten eventually (via Table.create() -> (Table.create() == Table(Timeout.typeid)) )?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Woah, talk about a new age of vJASS resources now made possible with Table 6. That said, I wanted to ask the following about the resource itself:

  • I noticed numerous direct accesses to the Table object Table(thistype.typeid). Wouldn't the allocation schema of Table objects put the aforementioned object above at risk of being overwritten eventually (via Table.create() -> (Table.create() == Table(Timeout.typeid)) )?
Since Table indexing starts at 8190, there is only a risk if there are more than 8190 structs + keys + function interfaces in the map (these are the things that generate into that key space). The 8190 is configurable though, to avoid any potential risk. It would have been nice if JassHelper had provided a "createKey" API I could call at the time of initialization and use that instead of the guesswork at 8190.
 
Last edited:
Top