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

Issues with timer functions

Status
Not open for further replies.
Level 9
Joined
Jul 20, 2018
Messages
176
Hi everyone!

There is a small research on how timer API works in WarCraft 3. Tests were made on 1.26a and 1.32.10 versions.



TimerGetTimeout

Whenever passed timer is started by TimerStart, this function returns the last passed timeout value. Example: timer is started by TimerStart with x seconds timeout. TimerGetTimeout returns x for this timer. After some time this timer is started again by TimerStart for y seconds. After that TimerGetTimeout returns y for it.

TimerGetRemaining

Returns remaining time of passed timer while timer is running or paused by PauseTimer. But after non-periodic timer expires, this function returns remaining time that was at last pause of this timer. Example: non-periodic timer was last time paused when x seconds remains to expire. When this timer expired, TimerGetRemaining would return x.

TimerGetElapsed

If passed timer is paused or has expired, this function returns (TimerGetTimeout - TimerGetRemaining). If passed timer was resumed by ResumeTimer, this function returns amount of time elapsed after last resuming.

PauseTimer

Pauses passed timer. If passed timer is non-periodic, after this timer expires, TimerGetRemaining returns value that was at last pause. Does not have such effect on periodic timers.

ResumeTimer

Has no effect if passed timer is running. If passed timer is paused or has expired, launches it for TimerGetRemaining, and after this time is elapsed, launches it again for TimerGetTimeout. After that passed timer is stopped even if it is periodic.

DestroyTimer

Destroys passed timer. Destroying does not pause timer, so if call of its callback is scheduled, then callback is called with GetElapsedTimer being null. Thus, such situation can happen: timer is destroyed, all data in hashtables connected with this timer is cleared, but its callback is still called.



How to overcome issues​

TimerGetRemaining

Track whether timer is expired. When it is expired, return 0, otherwise value from TimerGetRemaining.

TimerGetElapsed

Use TimerGetTimeout - TimerGetRemaining, where TimerGetRemaining is a fixed version.

PauseTimer

Use TimerStart(t, 0.0, false, null) to pause non-periodic timers or use fixed version of TimerGetRemaining.

ResumeTimer

Just write your own function for resuming.

DesroyTimer

Call PauseTimer before destroying.



I have attached an archive with:
  • a test map where you can easily check statements above;
  • message log files generated by test map on 1.26a and 1.32.10 versions;
  • raw timer library on Lua where all issues are resolved.


JASS:
function DebugMsg takes string s returns nothing
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0., 0., 5., s)
endfunction

function R2SX takes real r returns string
    return R2SW(r, 0, -1)
endfunction
JASS:
function GetT1Conf takes nothing returns nothing
    call ClearTextMessages()
    call DebugMsg("\nRemaining: " + R2SX(TimerGetRemaining(T1)))
    call DebugMsg("Elapsed: " + R2SX(TimerGetElapsed(T1)))
    call DebugMsg("Timeout: " + R2SX(TimerGetTimeout(T1)))
endfunction

function callback1 takes nothing returns nothing
    set Counter1 = Counter1 + 1
    call DebugMsg("\nRemaining in callback: " + R2SX(TimerGetRemaining(T1))) // outputs time remaining that was during last pause.
    call DebugMsg("Elapsed in callback: " + R2SX(TimerGetElapsed(T1))) // outputs TimerGetTimeout() - TimerGetRemaining().
    call DebugMsg("Timeout in callback: " + R2SX(TimerGetTimeout(T1))) // correct.
    call DebugMsg(I2S(Counter1))
endfunction

function f5 takes nothing returns nothing
    call TimerStart(T1, 1., Periodic, function callback1)
    call DebugMsg("\nResumed")
    call GetT1Conf()
endfunction

function f4 takes nothing returns nothing
    call PauseTimer(T1)
    call DebugMsg("\nPaused")
    call GetT1Conf()
    call TimerStart(T3, 1., false, function f5)
endfunction

function f3 takes nothing returns nothing
    call TimerStart(T1, 8., Periodic, function callback1)
    call TimerStart(T3, 2., false, function f4)
endfunction

function f2 takes nothing returns nothing
    call TimerStart(T1, 1., Periodic, function callback1)
    call DebugMsg("\nResumed")
    call GetT1Conf()
    call TimerStart(T3, 2., false, function f3)
endfunction

function f1 takes nothing returns nothing
    call PauseTimer(T1)
    call DebugMsg("\nPaused")
    call GetT1Conf()
    call TimerStart(T3, 1., false, function f2)
endfunction

function TimerTest_Actions takes nothing returns nothing
    call TimerStart(T1, 8., Periodic, function callback1)
    call TimerStart(T3, 1., false, function f1)
endfunction

function InitTrig_TimerTest takes nothing returns nothing
    set gg_trg_TimerTest = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_TimerTest, 1., false)
    call TriggerAddAction( gg_trg_TimerTest, function TimerTest_Actions )
endfunction

globals
    constant timer T1 = CreateTimer()
    constant timer T3 = CreateTimer()
    integer Counter1 = 0
    boolean Periodic = false // no bugs for 'true' value.
endglobals
JASS:
function callback3 takes nothing returns nothing
    call ClearTextMessages()
    call DebugMsg("\n\nRemaining: " + R2SX(TimerGetRemaining(T1)) + "\nElapsed: " + R2SX(TimerGetElapsed(T1)) + "\nTimeout: " + R2SX(TimerGetTimeout(T1)))
endfunction

function callback1 takes nothing returns nothing
    set Counter1 = Counter1 + 1
    call DebugMsg("\nRemaining in callback: " + R2SX(TimerGetRemaining(T1))) // outputs 0.
    call DebugMsg("Elapsed in callback: " + R2SX(TimerGetElapsed(T1))) // outputs 8.
    call DebugMsg("Timeout in callback: " + R2SX(TimerGetTimeout(T1))) // outputs 8.
    call DebugMsg("Counter: " + I2S(Counter1))
    call ResumeTimer(T1) // Runs timer for 0 and then immediately for timeout value, then again for 0 and so on.
    // Call is 'zero' run is ignored since timer is running.
endfunction

function TimerTest_Actions takes nothing returns nothing
    call TimerStart(T1, 8., false, function callback1)
    call TimerStart(T3, 1. / 2., true, function callback3)
endfunction

function InitTrig_TimerTest takes nothing returns nothing
    set gg_trg_TimerTest = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_TimerTest, 1., false)
    call TriggerAddAction( gg_trg_TimerTest, function TimerTest_Actions )
endfunction

globals
    constant timer T1 = CreateTimer()
    constant timer T3 = CreateTimer()
    integer Counter1 = 0
endglobals
JASS:
function callback1 takes nothing returns nothing
    set Counter1 = Counter1 + 1
    call DebugMsg("\nRemaining in callback: " + R2SX(TimerGetRemaining(T1))) // outputs time remaining that was during last pause.
    call DebugMsg("Elapsed in callback: " + R2SX(TimerGetElapsed(T1))) // outputs TimerGetTimeout() - TimerGetRemaining().
    call DebugMsg("Timeout in callback: " + R2SX(TimerGetTimeout(T1))) // outputs last given timeout.
    call DebugMsg("Counter: " + I2S(Counter1))
endfunction

function callback3 takes nothing returns nothing
    call ClearTextMessages()
    set Counter3 = Counter3 + 1
    call DebugMsg("\nRemaining: " + R2SX(TimerGetRemaining(T1)))
    call DebugMsg("Elapsed: " + R2SX(TimerGetElapsed(T1))) // when T1 paused, outputs (TimerGetTimeout() - TimerGetRemaining()).
    // When resumed, outputs amount of time after last resuming.
    // The same behaviour for periodic timer.
    call DebugMsg("Timeout: " + R2SX(TimerGetTimeout(T1))) // outputs last given timeout.
    if Counter3 == 3 or Counter3 == 7 or Counter3 == 17 then
        call PauseTimer(T1)
        call DebugMsg("\nPaused")
    elseif Counter3 == 5 or Counter3 == 15 or Counter3 == 19 then
        call ResumeTimer(T1) // Launches T1 for TimerGetRemaining(), and after T1 expires, T1 will be started again for TimerGetTimeout().
        call DebugMsg("\nResumed")
    elseif Counter3 == 9 then
        call TimerStart(T1, 1., false, function callback1)
        call DebugMsg("\nStarted again")
    elseif Counter3 == 32 then
        call PauseTimer(T3)
    endif
endfunction

function TimerTest_Actions takes nothing returns nothing
    call TimerStart(T1, 8., false, function callback1)
    call TimerStart(T3, 1. / 2., true, function callback3)
endfunction

function InitTrig_TimerTest takes nothing returns nothing
    set gg_trg_TimerTest = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_TimerTest, 1., false)
    call TriggerAddAction( gg_trg_TimerTest, function TimerTest_Actions )
endfunction

globals
    constant timer T1 = CreateTimer()
    constant timer T3 = CreateTimer()
    integer Counter1 = 0
    integer Counter3 = 0
endglobals
JASS:
function callback3 takes nothing returns nothing
    call ClearTextMessages()
    set Counter3 = Counter3 + 1
    call DebugMsg("\nRemaining: " + R2SX(TimerGetRemaining(T1)))
    call DebugMsg("Elapsed: " + R2SX(TimerGetElapsed(T1)))
    call DebugMsg("Timeout: " + R2SX(TimerGetTimeout(T1)))
    if DO then
    if Counter3 == 20 then
        call ResumeTimer(T1) // Launches T1 for TimerGetRemaining() (it's0 because T1 was paused in destination function),
        // and after T1 expires, T1 will be started again for last given timeout.
        call DebugMsg("\nResumed")
        set DO = false
    endif
    endif
endfunction

function callback1 takes nothing returns nothing
    set Counter1 = Counter1 + 1
    call DebugMsg("\nRemaining in callback: " + R2SX(TimerGetRemaining(T1))) // everything fine here
    call DebugMsg("Elapsed in callback: " + R2SX(TimerGetElapsed(T1)))
    call DebugMsg("Timeout in callback: " + R2SX(TimerGetTimeout(T1)))
    call DebugMsg("Counter: " + I2S(Counter1))
    call PauseTimer(T1)
endfunction

function TimerTest_Actions takes nothing returns nothing
    call TimerStart(T1, 8., true, function callback1)
    call TimerStart(T3, 1. / 2., true, function callback3)
endfunction

function InitTrig_TimerTest takes nothing returns nothing
    set gg_trg_TimerTest = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_TimerTest, 1., false)
    call TriggerAddAction( gg_trg_TimerTest, function TimerTest_Actions )
endfunction

globals
    constant timer T1 = CreateTimer()
    constant timer T3 = CreateTimer()
    boolean DO = true
    integer Counter1 = 0
    integer Counter3 = 0
endglobals
JASS:
function callback3 takes nothing returns nothing
    call ClearTextMessages()
    call DebugMsg("\n\nNonperiodic Timer\nRemaining: " + R2SX(TimerGetRemaining(T1)) + "\nElapsed: " + R2SX(TimerGetElapsed(T1)) + "\nTimeout: " + R2SX(TimerGetTimeout(T1)) + "\nData: " + I2S(R2I(TimerGetRemaining(T1) + 0.5)))
    call DebugMsg("\nPeriodic Timer\nRemaining: " + R2SX(TimerGetRemaining(T2)) + "\nElapsed: " + R2SX(TimerGetElapsed(T2)) + "\nTimeout: " + R2SX(TimerGetTimeout(T2)) + "\nData: " + I2S(R2I(TimerGetRemaining(T2) + 0.5)))
endfunction

function callback2 takes nothing returns nothing
    call DebugMsg("\nRemaining in callback2: " + R2SX(TimerGetRemaining(T2)))
    call DebugMsg("Elapsed in callback2: " + R2SX(TimerGetElapsed(T2)))
    call DebugMsg("Timeout in callback2: " + R2SX(TimerGetTimeout(T2)))
endfunction

function callback1 takes nothing returns nothing
    call DebugMsg("\nRemaining in callback1: " + R2SX(TimerGetRemaining(T1))) // outputs saved value.
    call DebugMsg("Elapsed in callback1: " + R2SX(TimerGetElapsed(T1))) // ouputs 8 - saved value.
    call DebugMsg("Timeout in callback1: " + R2SX(TimerGetTimeout(T1))) // outputs 8.
    call ResumeTimer(T1) // Runs timer for saved value, then for timeout value, then for saved and so on.
endfunction

function TimerExploit takes timer t, real period, integer data, boolean periodic, code callb returns nothing
    call TimerStart(t, data, false, null)
    call PauseTimer(t)
    call TimerStart(t, period, periodic, callb)
endfunction

function Trig_TimerExploitTest_Actions takes nothing returns nothing
    call TimerExploit(T1, 8., 5, false, function callback1)
    call TimerExploit(T2, 8., 6, true, function callback2) // exploit does not work for periodic timers.
    call TimerStart(T3, 1. / 2., true, function callback3)
endfunction

function InitTrig_TimerExploitTest takes nothing returns nothing
    set gg_trg_TimerExploitTest = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_TimerExploitTest, 1., false)
    call TriggerAddAction( gg_trg_TimerExploitTest, function Trig_TimerExploitTest_Actions )
endfunction

globals
    constant timer T1 = CreateTimer()
    constant timer T2 = CreateTimer()
    constant timer T3 = CreateTimer()
endglobals
 

Attachments

  • TimerAPI.7z
    20.1 KB · Views: 26
Last edited:
Level 9
Joined
Jul 20, 2018
Messages
176
This is the only statement without proof
Finally found proof.
JASS:
function DebugMsg takes string s returns nothing
    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0., 0., 5., s)
endfunction

globals
    timer MyTimer = CreateTimer()
endglobals

function MyMessage takes nothing returns nothing
    call DebugMsg("Message!")
    call DebugMsg("HandleId of expired timer is " + I2S(GetHandleId(GetExpiredTimer())))
endfunction

function DestroyMyTimer takes nothing returns nothing
    call DestroyTimer(MyTimer)
    call DebugMsg("MyTimer destroyed")
    call DestroyTimer(GetExpiredTimer())
endfunction

function TestTimers takes nothing returns nothing
    call TimerStart(CreateTimer(), 10., false, function DestroyMyTimer)
    call TimerStart(MyTimer, 10., false, function MyMessage)
endfunction

Tested in Reforged, issue exists. Another way is putting a very small period for periodic timer. In 1.26 1 / 512 give one excess tick, but not in Reforged. In Reforged the smallest denormal gives excess 45 ticks.
 

Attachments

  • DestroyTimerTest.w3x
    8 KB · Views: 25
  • ExcessTimerTicks reforged.w3x
    8.1 KB · Views: 36
  • ExcessTimerTicks.w3x
    7.9 KB · Views: 25
Last edited:
Status
Not open for further replies.
Top