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

[vJASS] TriggerSleepAction(0.00) vs. Timer with 0.00 second timeout

Status
Not open for further replies.
Level 39
Joined
Feb 27, 2007
Messages
5,031
I'm catching a unit's spell cast, giving it a different ability, and then forcing the owning player to press its hotkey. If USE_TSA == true the game prompts me to cast the second spell as I expect, but with the 0 second timer it doesn't work. Why? I used FRAPS to confirm I'm running at 60 fps, so it's not like it needs a frame to update something (TIMEOUT = 1.00/60 = 0.017, which is below the 'break' threshhold).
JASS:
    private function TEST2 takes nothing returns nothing
        call ForceUIKey(NEXT_HKY)
    endfunction
  
    private function TEST takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local boolean USE_TSA = true
        local real TIMEOUT = 0.00

        call UnitRemoveAbility(u, MAIN_AID)
        call UnitAddAbility(u, NEXT_AID)

        if USE_TSA then
            //Works with TIMEOUT == 0.00
            call TriggerSleepAction(TIMEOUT)
            call ForceUIKey(NEXT_HKY)
        else
            //Doesn't work if TIMEOUT < 0.20 and sometimes doesn't work at 0.20 too (but not always)
            call TimerStart(NewTimer(), TIMEOUT, false, function TEST2)
        endif
    endfunction
  
    private function Init takes nothing returns nothing
        local trigger T = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddAction(T, function TEST)
    endfunction
 
Level 13
Joined
Mar 24, 2013
Messages
1,105
You can use the command /fps in wc3 to show FPS.

Is there any reason why you even need a timer? Why cant you force the hotkey press right away?

Also if you want to preserve the cooldown of the first skill you can simply disable it for that player rather than removing.
 
Level 39
Joined
Feb 27, 2007
Messages
5,031
Is there any reason why you even need a timer? Why cant you force the hotkey press right away?
You inspired me to try some new things and it seems it has something to do with the UI needing to update. If the units already have the ability for the hotkey I'm forcing then it works fine in the main function... but if they don't the delay is necessary. Even if the ability is on the unit but disabled and just enabled when necessary it has some sort of update/pause needed.

There's one other particularly anomalous thing I can't understand: if I chain this together to keep recasting NEXT_AID ad infinitum it works with METHOD_NEW below unless you click fast enough. Adding a long cast time to the point target ability seemed to make the probability of this happening go down but it's impossible to eliminate it! Changing to METHOD_TSA literally just adds in a TSA(0) call but magically it prevents this from ever happening!! Or at least I can't click fast enough to break it.

Check out the test map and mess with it if you wanna see. Change USED_METHOD to the different variants. If you see a purple spinning effect, the 'next point' ability successfully cast... somehow.
JASS:
library MPTS initializer Init requires GroupUtils, LinkedList, xefx, xepreload
    //Issues: multiple click sounds, don't like lockout method, effects don't show in FoW
 
    globals
        private constant integer MAIN_AID = 'A004'
        private constant integer NEXT_AID = 'A005'
        private constant integer CNCL_AID = 'A003'
        private constant integer BUFF_AID = 'A001'
        private constant integer BUFF_BID = 'B000'
        private          integer NEXT_ORD //= OrderId("rainoffire")
        private          integer CNCL_ORD //= OrderId("taunt")
        private          integer BUFF_ORD //= OrderId("cripple")
        private constant string  NEXT_HKY = "j" //always lowercase
        private constant string  CNCL_HKY = "l"
     
        private constant string  INDICATOR_EFFECT = "Objects\\InventoryItems\\BattleStandard\\BattleStandard.mdl"
        private constant real    INDICATOR_FACING = 270.00*bj_PI/180.00
             
        private constant real    CheckInterval = 0.05
        private constant real    LOCKOUT       = 1.00
        private constant real    CastHotkeyPressDelay = 0.05

        private constant integer METHOD_TSA        = 0
        private constant integer METHOD_TIMER_0    = 1
        private constant integer METHOD_TIMER    = 2
        private constant integer METHOD_EXECUTE    = 3
        private constant integer METHOD_NEW        = 4
       @private constant integer USED_METHOD    = METHOD_NEW @
     
        private          group         Casting
        private          integer array NumCasting
        private          integer       LP                       //GetLocalPlayer() stored
        private          timer         CancelTimer
        private          List          Instances
        private          hashtable     HT
        private constant integer       INSTANCE_STRUCT = 0
    endglobals

 
//constant function Flare_DummyUnitId takes nothing returns integer
//    return 'h000' //Must have 0 cast point and cast backswing
//endfunction

 
 
    private struct target
        unit u
        real x
        real y
        real z
        xefx f
     
        method onDestroy takes nothing returns nothing
           call .f.hiddenDestroy()
        endmethod
     
        static method create takes unit u, real x, real y, real z, string fx returns target
            local target t = target.allocate()
            set t.u = u
            set t.x = x
            set t.y = y
            set t.z = z
            set t.f = xefx.create(x,y,INDICATOR_FACING)
            set t.f.fxpath = fx
            return t
        endmethod
    endstruct

    private struct CastData
        unit    u
        integer p
        integer lvl
        boolean unp
        string  fxp
        List    tgts
        integer xNoDetect
     
        static CastData TMP

        static method CancelCheck takes nothing returns nothing
            local integer i = 0
            local Link l = Instances.first
            local CastData cd
            loop
                exitwhen l == 0
                set cd = CastData(l.data)
                if cd.NoDetect > 0 then
                    set cd.NoDetect = cd.NoDetect-1
                endif
                set l = l.next
            endloop

            if NumCasting[LP] > 0 then
                call ForceUIKey(CNCL_HKY)
            endif
        endmethod
 
        static method ForceKeyCallback takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local CastData cd = CastData(GetTimerData(t))
     
            //set cd.NoDetect = R2I(LOCKOUT/CheckInterval+0.50)
            if LP == cd.p then
                call ForceUIKey(NEXT_HKY)
            endif
     
            call ReleaseTimer(t)
            set t = null
        endmethod
     
        static method ForceKeyCallbackEx takes nothing returns nothing
            local CastData cd = TMP
         
            //set cd.NoDetect = R2I(LOCKOUT/CheckInterval+0.50)
            //call TriggerSleepAction(0.00)
            //call PauseUnit(cd.u, true)
            //call IssueImmediateOrder(cd.u, "stop")
            //call PauseUnit(cd.u, false)
            if LP == cd.p then
                call ForceUIKey(NEXT_HKY)
            endif
        endmethod

        method AddTargetXY takes real x, real y returns nothing
            call Link.create(.tgts, target.create(null, x, y, 0.00, .fxp))
            call BJDebugMsg("Target "+I2S(.tgts.size)+": ("+R2S(target(.tgts.first.data).x)+", "+R2S(target(.tgts.first.data).y)+")")
        endmethod
     
        @method SelectXY takes integer how returns nothing@
            set .NoDetect = R2I(LOCKOUT/CheckInterval+0.50)
         
            if how == METHOD_TSA then
                call TriggerSleepAction(0.00)
                if LP == .p then
                    call ForceUIKey(NEXT_HKY)
                endif
            elseif how == METHOD_TIMER_0 then
                //set .NoDetect = R2I(LOCKOUT/CheckInterval+0.50)
                call TimerStart(NewTimerEx(this), 0.00, false, function CastData.ForceKeyCallback)
            elseif how == METHOD_TIMER then
                call TimerStart(NewTimerEx(this), CastHotkeyPressDelay, false, function CastData.ForceKeyCallback)
            elseif how == METHOD_EXECUTE then
                set TMP = this
                call CastData.ForceKeyCallbackEx.execute()
//                call ExecuteFunc(CastData.ForceKeyCallbackEx.name)
            elseif how == METHOD_NEW then
                if LP == .p then
                    call ForceUIKey(NEXT_HKY)
                endif
            endif
        @endmethod@
     
//        method AddTargetUnit takes unit u returns nothing
//        endmethod
     
        method operator NoDetect takes nothing returns integer
            return .xNoDetect
        endmethod
     
        method operator NoDetect= takes integer i returns nothing
            local integer prev = .xNoDetect
            set .xNoDetect = i

           if i > prev then
                if prev == 0 then
                    call .RemoveFromStack()
                endif
            elseif i < prev and i >= 0 then
                if i == 0 then
                    call .AddToStack()
                endif
            endif
        endmethod
     
        method AddToStack takes nothing returns nothing
            if .NoDetect == 0 then
                set NumCasting[.p] = NumCasting[.p]+1
            endif
            if Instances.size == 1 then
               call TimerStart(CancelTimer, CheckInterval, true, function CastData.CancelCheck)
            endif
        endmethod
     
        method RemoveFromStack takes nothing returns nothing
            if .NoDetect == 0 then
                set NumCasting[.p] = NumCasting[.p]-1
            endif
            if Instances.size == 0 then
               call PauseTimer(CancelTimer)
                call GroupRefresh(Casting)
            endif
        endmethod
     
        method AddAbilities takes nothing returns nothing
            call SaveInteger(HT, INSTANCE_STRUCT, GetHandleId(.u), this)
            call GroupAddUnit(Casting, .u)
//            call UnitAddAbility(.u, NEXT_AID)
//            call UnitAddAbility(.u, CNCL_AID)
            call SetPlayerAbilityAvailable(Player(.p), NEXT_AID, true)
            call SetPlayerAbilityAvailable(Player(.p), CNCL_AID, true)
        endmethod

        method RemoveAbilities takes nothing returns nothing
            call RemoveSavedInteger(HT, INSTANCE_STRUCT, GetHandleId(.u))
            call GroupRemoveUnit(Casting, .u)
//            call UnitRemoveAbility(.u, NEXT_AID)
//            call UnitRemoveAbility(.u, CNCL_AID)
//            call SetPlayerAbilityAvailable(Player(.p), NEXT_AID, false)
//            call SetPlayerAbilityAvailable(Player(.p), CNCL_AID, false)
        endmethod
     
        method RefreshMain takes nothing returns nothing
//            call SetPlayerAbilityAvailable(Player(.p), MAIN_AID, false)
//            call SetPlayerAbilityAvailable(Player(.p), MAIN_AID, true)
            call UnitRemoveAbility(.u, MAIN_AID) 
            call UnitAddAbility(.u, MAIN_AID)
            call SetUnitAbilityLevel(.u, MAIN_AID, .lvl)
            call UnitMakeAbilityPermanent(.u, true, MAIN_AID)
//            call SetUnitState(.u, UNIT_STATE_MANA, GetUnitState(U, UNIT_STATE_MANA)+Flare_ManaCost(Level))
        endmethod
     
        method RemoveInstance takes nothing returns nothing
            local Link l = .tgts.first

            call .RemoveAbilities()
            call .RefreshMain()
//            if .unp then //could be simpler
//                call PauseUnit(.u, false)
//            endif

            loop
                exitwhen l == 0
                call target(l.data).destroy()
                set l = l.next
            endloop
            call .tgts.destroy()
         
            call Instances.search(this).destroy()
            call .RemoveFromStack()
        endmethod
     
        static method CreateInstance takes unit u, string fx, boolean pause returns CastData
            local CastData cd = CastData.allocate()
         
            set cd.u = u
            set cd.p = GetPlayerId(GetOwningPlayer(u))
            set cd.lvl = GetUnitAbilityLevel(cd.u, MAIN_AID)
            set cd.unp = pause
            set cd.tgts = List.create()
            if LP == cd.p then
               set cd.fxp = fx
            else
                set cd.fxp = ""
            endif

            //call UnitRemoveAbility(cd.u, MAIN_AID) 
            //call UnitAddAbility(cd.u, MAIN_AID)
            //call SetUnitAbilityLevel(cd.u, MAIN_AID, cd.lvl)
            //call UnitMakeAbilityPermanent(.u, true, MAIN_AID)
            //call IssueImmediateOrder(cd.u,"stop")
            //call ForceUIKey(NEXT_HKY)
         
//            if cd.unp then //could be simpler
//                call PauseUnit(cd.u, true)
//            endif
         
            call Link.create(Instances, cd)
            call cd.AddAbilities()
            call cd.AddToStack()
         
            return cd
        endmethod
    endstruct

    private function CancelCast takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local CastData cd
        call BJDebugMsg("None order: "+I2S(GetIssuedOrderId())+OrderId2String(GetIssuedOrderId()))

        if GetIssuedOrderId() == CNCL_ORD and IsUnitInGroup(u, Casting) then
            set cd = CastData(LoadInteger(HT, INSTANCE_STRUCT, GetHandleId(u)))
            if cd.NoDetect == 0 then
                call cd.RemoveInstance()
            endif
        endif
     
        set u = null
    endfunction

    private function NextPointCast takes nothing returns nothing
       local unit u = GetTriggerUnit()
        local CastData cd
     
        call BJDebugMsg("Point order: "+I2S(GetIssuedOrderId())+OrderId2String(GetIssuedOrderId()))
     
        if GetIssuedOrderId() == NEXT_ORD and IsUnitInGroup(u, Casting) then
            set cd = CastData(LoadInteger(HT, INSTANCE_STRUCT, GetHandleId(u)))
            call IssueImmediateOrder(cd.u,"stop")
            call cd.AddTargetXY(GetOrderPointX(), GetOrderPointY())
            call cd.SelectXY(USED_METHOD)
        endif
    endfunction         
 
    private function MainCast takes nothing returns nothing
       local integer Aid = GetSpellAbilityId()
        local CastData cd
     
        if Aid == MAIN_AID then
            set cd = CastData.CreateInstance(GetTriggerUnit(), INDICATOR_EFFECT, false)
            call cd.AddTargetXY(GetSpellTargetX(),GetSpellTargetY())//GetOrderPointX(),GetOrderPointY())
            call cd.SelectXY(USED_METHOD)
        endif     
    endfunction

    function Death takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local CastData cd
        if IsUnitInGroup(u, Casting) then
            set cd = CastData(LoadInteger(HT, INSTANCE_STRUCT, GetHandleId(u)))
            call cd.RemoveInstance()
        endif
    endfunction
 
    private function TEST2 takes nothing returns nothing
        call ForceUIKey(NEXT_HKY)
    endfunction
 
    private function TEST takes nothing returns nothing
        local unit u = GetTriggerUnit()
        call UnitRemoveAbility(u, MAIN_AID)
        call UnitAddAbility(u, NEXT_AID)
//        call TriggerSleepAction(0.00)
//        call ForceUIKey(NEXT_HKY)
        call TimerStart(NewTimer(), 0.025, false, function TEST2)
    endfunction
 
    private function Init takes nothing returns nothing
        local trigger T = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_SPELL_CAST)//_EFFECT)
        call TriggerAddAction(T, function MainCast)
     
//        set  T = CreateTrigger()
//        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_SPELL_EFFECT)
//        call TriggerAddAction(T, function TEST)

        set  T = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
        call TriggerAddAction(T, function NextPointCast)

        set  T = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_ISSUED_ORDER)//EVENT_PLAYER_UNIT_SPELL_CAST)
        call TriggerAddAction(T, function CancelCast)
     
        set T = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddAction(T, function Death)

        set T = null
        set Casting = NewGroup()
        set LP = GetPlayerId(GetLocalPlayer())
        set Instances = List.create()
        set HT = InitHashtable()
        set CancelTimer = NewTimer()
     
        call XE_PreloadAbility(MAIN_AID)
        call XE_PreloadAbility(CNCL_AID)
        call XE_PreloadAbility(NEXT_AID)
     
        set CNCL_ORD = OrderId("taunt")
        set NEXT_ORD = OrderId("rainoffire")
        set BUFF_ORD = OrderId("cripple")
    endfunction
endlibrary
 

Attachments

  • Flare Redux.w3x
    179 KB · Views: 60
Last edited:

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,202
TriggerSleepAction is limited at best to 0.1 second timeout according to the documentation. This is even worse in multiplayer since it appears netsynced so it has latency further added to it. The result is that its timeout is only approximate, and not really predictable and is always multiple frames.

Timers on the other hand are pretty much absolute. A timeout of 0 seconds will fire as soon as the current thread, and all queued threads, return. There still is a delay, multiple events might fire before the timeout does.
 
Level 39
Joined
Feb 27, 2007
Messages
5,031
TriggerSleepAction is limited at best to 0.1 second timeout according to the documentation. This is even worse in multiplayer since it appears netsynced so it has latency further added to it. The result is that its timeout is only approximate, and not really predictable and is always multiple frames.

Timers on the other hand are pretty much absolute. A timeout of 0 seconds will fire as soon as the current thread, and all queued threads, return. There still is a delay, multiple events might fire before the timeout does.
I know the difference and why TSA is bad. I just don't understand why a 0.0 or 0.01 timeout timer doesn't work to force the key (I initially thought the TSA(0) didn't pause at all).
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
Might be too small a delay. If it really depends on frame update, that can be spiky, even if you see 60fps, especially if the ability is initialized there. Waits are usually >0.2 seconds and who knows, because of their inaccuracies/local behavior, they are sync matter and thus maybe trigger an update, too.
 
Status
Not open for further replies.
Top