System Key Timers 2

SanKakU

Member
Reaction score
21
yes, the unit is mobile...anyways...wow man could you do me a huge favor and explain to me what part of the huge chunks of code is DIFFERENT? or at least post in coded box, as an exception...jass boxes are impossible to read on a forum...i hate them! try to remember that when helping me if at all possible. hmm...i guess i'll try to put it in my signature or something.
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
Okay as written in this post I got a frequency issue with KT(2). You wanted the script so here you go.

JASS:
scope Camlock initializer InitTrig
globals
    boolean CAMLOCK_ON=false
    boolean array DEATH
    private constant real FREQUENCY=0.02 // if changed to 0.015 (and some other values) it doesn't work after some time eventually. Especially after spells involving KT were used. 
    private constant real DIST=200 
endglobals
struct Camlock
    // just to use KT
endstruct
private function Callback takes nothing returns boolean
    local integer index=0
    local real cam_height
    call SetCameraField(CAMERA_FIELD_FARZ,7500, 0)
    if CAMLOCK_ON==true then
        loop
            exitwhen index==MAX_PLAYERS
            if ON[index] then
                call MoveLocation(LOC,GetUnitX(CAM_HERO[index]),GetUnitY(CAM_HERO[index]))
                set cam_height=GetLocationZ(LOC)+DIST+GetUnitFlyHeight(CAM_HERO[index])
                if GetLocalPlayer() == PLAYERS[index] then
                    call SetCameraField(CAMERA_FIELD_ZOFFSET,GetCameraField(CAMERA_FIELD_ZOFFSET)+cam_height-GetCameraTargetPositionZ(),0.00)
                    call SetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK,315,0.01)
                    call SetCameraField(CAMERA_FIELD_ROTATION,140,0.01)
                endif
            endif
            set index = index + 1
        endloop
    endif
    return false
endfunction

private function Actions takes nothing returns nothing
    local Camlock c = Camlock.create()
    call KT_Add(function Callback,c,FREQUENCY)
endfunction

private function InitTrig takes nothing returns nothing
    local trigger trig = CreateTrigger()
    call TriggerRegisterTimerEvent(trig,1,false)
    call TriggerAddAction(trig, function Actions)
endfunction
endscope
 

Jesus4Lyf

Good Idea™
Reaction score
397
Intriguing.
JASS:
//    =Cons=
//         - The code passed into KT2 must call KT_GetData exactly once.
//         - Periods must be a multiple of 0.00125 seconds. Not 0.007, for example.

Here's the part of the documentation that mentions calling [LJASS]KT_GetData()[/LJASS] once (you haven't).

If you require further assistance, please submit code which compiles (some constants aren't declared in there).
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
Ah forgot about that part. But it's weird, since it really works well with 0.02frequency, thus I didn't expect that^^
Thanks for the help.

I was quite astonished btw. that this way is faster than basing it on a trigger that runs every 0.02 seconds or so
 

tooltiperror

Super Moderator
Reaction score
231
I think I`m going to use this.

Good Job, Jesus.

(Reputation.)
 

tooltiperror

Super Moderator
Reaction score
231
Right, so, just wondering, if we update the struct during the callback, is the new struct used if it is a repeating callback?
 

tooltiperror

Super Moderator
Reaction score
231
(userFunc, struct, period)

JASS:

//! fix alignment
  library KeyTimerTesting requires KT initializer onInit
      module Tick
          integer tick=0
      endmodule
      struct Data
          implement Tick // Random question, is that how modules work?
      endstruct 
      function callback takes nothing returns boolean
          local Data data=KT_GetData()
          set data.tick=data.tick+1
            if data.tick==2
              return true
            endif     
          return false
      endfunction
      function onInit takes nothing returns nothing
          local Data data=Data.create()
          call KT_Add(function callback,data,3.00)
      endfunction
  endlibrary


This is hard to explain, but will the tick work, even though it has changed since you started the periodic?
 

Bribe

vJass errors are legion
Reaction score
67
A module is a pack of struct members. If you have some common variables you like to use, such as a group, an array, and an integer to point the size of the array, and you are tired of retyping them, you can do this:

JASS:
module Pack
    thistype array stack
    integer size = 0
    group Zerglings = CreateGroup()
endmodule

struct A
    implement Pack
endstruct

struct B
    implement Pack
endstruct

struct C
    implement Pack
endstruct


Gives each structs A-C their own copies of those three variables. Vexorians' words: think of a module like an advanced textmacro.

The neatest advantages of modules over textmacros are that the modules can inherit from the library they are first made in, they can have private members that can't be read by the struct implementing it, and they also inherit from the library/struct they are placed in. Hence the use of "thistype" for a module referencing its delegated type is essential.
 

Bribe

vJass errors are legion
Reaction score
67
I have a function request on this.

Though it's needed to call KT_GetData() once, I have no use for the integer in my implentation because I'm using static methods and always returning false.

JASS:

function KT_Static takes nothing returns nothing
    set t_mem = t_mem<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite7" alt=":p" title="Stick Out Tongue    :p" loading="lazy" data-shortname=":p" />rev
endfunction


This way, it inlines and it's compatible.
 

Bribe

vJass errors are legion
Reaction score
67
This has an improved engine; no longer required to call KT_GetData, no longer need to destroy boolean expressions. This uses the GroupUtils method of recycling boolean expressions.

Things I suggest changing/adding have a "B" at the start of the line;

JASS:

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~ KT ~~ Key Timers 2 ~~ By Jesus4Lyf ~~ Version 1.8.0 ~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//  What is Key Timers?
//         - Key Timers attaches structs to timers, or more to the point timed
//           effects.
//         - You can specify different periods.
//         - Key Timers only uses one timer with one trigger per low period
//           to keep things efficient, especially within the looping.
//         - Key Timers alternatively uses one trigger per instance for all higher
//           periods to allow accurate expirations in a stable and efficient fashion.
//
//    =Pros=
//         - Easy to use.
//         - Fastest attachment loading system (storing in parallel arrays).
//         - Fastest execution system for low periods (all functions on one trigger).
//         - Allows multiple periods to be used.
// Bribe   - No longer requires a KT_GetData() call.
//         - No H2I. Backwards compatability through patch 1.23 and 1.24.
//
//    =Cons=
  
//         - Periods must be a multiple of 0.00125 seconds. Not 0.007, for example.
//
//    Functions:
//         - KT_Add(userFunc, struct, period)
//         - KT_GetData returns the struct
//
//         - userFunc is to be a user function that takes nothing and returns boolean.
//           It will be executed by the system every period until it returns true.
//
//         - KT_GetData is to be used inside func; it will return the struct passed to
//           to the Add function.
//
//  Details:
//         - KT2 treats low periods and high periods differently, optimizing each
//           with appropriate speed and accuracy, although this effect is invisible
//           to you, the mapper.
//
//         - While func returns false the timer will continue to call it each period.
//           Once func returns true the instance will be detached from system.
//
//  Thanks:
//         - Daxtreme: For encouraging me to return to Key Timers 2, rather than
//           leave it to rot. His interest in the system restored it, and helped
//           it to become what it is now. <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite1" alt=":)" title="Smile    :)" loading="lazy" data-shortname=":)" />
//
//         - Captain Griffen: For his work on Rapid Timers, demonstrating that it
//           is possible to attach all functions to one trigger, and that it is
//           indeed faster.
//
//         - Cohadar: Told me to make Key Timers without a textmacro.
//           Thanks to him for helping me with the original Key Timers system too.
//           Also, I&#039;d like to thank him for his work on Timer Ticker (TT) which
//           demonstrated how to use triggers/conditions in this sort of system,
//           which has been used in Key Timers 2.
//
//  How to import:
//         - Create a trigger named KT.
//         - Convert it to custom text and replace the whole trigger text with this.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
library KT
    ///////////////
    // Constants //
    ////////////////////////////////////////////////////////////////////////////
    // That bit that users may play with if they know what they&#039;re doing.
    // Not touching these at all is recommended.
    globals
        // Period Threshold is the point at which Key Timers 2 will switch from
        // using the single timer per period mechanism to using TAZO, which is
        // better for higher periods due to the first tick being accurate.
        private constant real PERIODTHRESHOLD=0.3 // MUST be below 10.24 seconds.
        
        // Tazo&#039;s number of precached instances. You can go over this during
        // your map at runtime, but it will probably do some small background
        // processing. Precaching just speeds things up a bit.
        private constant integer TAZO_PRECACHE=64
        
        // Tazo uses the low period part of Key Timers 2 to construct triggers
        // over time when precached ones run out. Here you can set the period used.
        private constant real TAZO_CONSTRUCT_PERIOD=0.03125
    endglobals
    
    //////////////////////////
    // Previous KT2 Globals //
    ////////////////////////////////////////////////////////////////////////////
    // These needed to be moved here for TAZO to hook GetData.
    globals
/* B */ private hashtable table = InitHashtable()
        private timer array KeyTimer
        private trigger array TimerTrigger
        private integer array KeyTimerListPointer
        private integer array KeyTimerListEndPointer
        private triggercondition array TriggerCond
        private boolexpr array Boolexpr
        private integer array Data
        private integer array Next
        private integer array Prev
        private integer TrigMax=0
        private integer array NextMem
        private integer NextMemMaxPlusOne=1
        private integer array ToAddMem
        private triggercondition array ToRemove
// B    private boolexpr array ToDestroy
        private boolean array IsAdd
        private integer AddRemoveMax=0
        
        // Locals
        private integer t_id=-1
        private integer t_mem
        private integer t_lastmem
        private integer a_id
        private integer a_mem
        
        // Code Chunks
        private conditionfunc RemoveInstanceCond
/* B */ private conditionfunc GetDataCond
    endglobals
    
    //////////////////
    // Previous KT2 //
    ////////////////////////////////////////////////////////////////////////////
    // The KT2 implementation
    private function KeyTimerLoop takes nothing returns nothing
        set t_id=R2I(TimerGetTimeout(GetExpiredTimer())*800)
        set t_mem=KeyTimerListEndPointer[t_id]
        call TriggerEvaluate(TimerTrigger[t_id])
        set t_mem=0
        loop
            exitwhen t_mem==AddRemoveMax
            set t_mem=t_mem+1
            if IsAdd[t_mem] then
                set TriggerCond[ToAddMem[t_mem]]=TriggerAddCondition(TimerTrigger[t_id],Boolexpr[ToAddMem[t_mem]])
            else
                call TriggerRemoveCondition(TimerTrigger[t_id],ToRemove[t_mem])
// B            call DestroyBoolExpr(ToDestroy[t_mem])
            endif
        endloop
        set AddRemoveMax=0
        set t_id=-1
    endfunction
    
    private function RemoveInstance takes nothing returns boolean
        // Will only fire if code returns true.
        set AddRemoveMax=AddRemoveMax+1
        set IsAdd[AddRemoveMax]=false
        set ToRemove[AddRemoveMax]=TriggerCond[t_lastmem]
// B    set ToDestroy[AddRemoveMax]=Boolexpr[t_lastmem]
        if Next[t_lastmem]==0 then
            set KeyTimerListEndPointer[t_id]=Prev[t_lastmem]
        endif
        set Prev[Next[t_lastmem]]=Prev[t_lastmem]
        if Prev[t_lastmem]==0 then
            set KeyTimerListPointer[t_id]=Next[t_lastmem]
            if KeyTimerListPointer[t_id]&lt;1 then
                call PauseTimer(KeyTimer[t_id])
            endif
        else
            set Next[Prev[t_lastmem]]=Next[t_lastmem]
        endif
        set NextMem[NextMemMaxPlusOne]=t_lastmem
        set NextMemMaxPlusOne=NextMemMaxPlusOne+1
        return false
    endfunction
    
    private function KTadd takes code func, integer data, real period returns nothing
        set a_id=R2I(period*800)
        
        if KeyTimer[a_id]==null then
            set KeyTimer[a_id]=CreateTimer()
            set TimerTrigger[a_id]=CreateTrigger()
        endif
        
        if NextMemMaxPlusOne==1 then
            set TrigMax=TrigMax+1
            set a_mem=TrigMax
        else
            set NextMemMaxPlusOne=NextMemMaxPlusOne-1
            set a_mem=NextMem[NextMemMaxPlusOne]
        endif
        
/* B */ set Boolexpr[a_mem]=LoadBooleanExprHandle(table,&#039;FAST&#039;,GetHandleId(Condition(func)))
/* B */ if (Boolexpr[a_mem]==null) then
/* B */     set Boolexpr[a_mem]=Or(GetDataCond,And(Condition(func),RemoveInstanceCond))
/* B */     call SaveBooleanExprHandle(table,&#039;FAST&#039;,GetHandleId(Condition(func)),Boolexpr[a_mem])
/* B */ endif
        
        if t_id==a_id then
            set AddRemoveMax=AddRemoveMax+1
            set IsAdd[AddRemoveMax]=true
            set ToAddMem[AddRemoveMax]=a_mem
        else
            if KeyTimerListPointer[a_id]&lt;1 then
                call TimerStart(KeyTimer[a_id],a_id/800.0,true,function KeyTimerLoop)
                set KeyTimerListEndPointer[a_id]=a_mem
            endif
            
            set TriggerCond[a_mem]=TriggerAddCondition(TimerTrigger[a_id],Boolexpr[a_mem])
        endif
        set Data[a_mem]=data
        
        set Prev[a_mem]=0
        set Next[a_mem]=KeyTimerListPointer[a_id]
        set Prev[KeyTimerListPointer[a_id]]=a_mem
        set KeyTimerListPointer[a_id]=a_mem
    endfunction
    
    public function GetData takes nothing returns integer // Gets hooked by TAZO.
// B    set t_lastmem=t_mem
// B    set t_mem=Prev[t_mem]
/* B */ return Data[t_lastmem]  // Inlines!
    endfunction
    
/**/private function GetDataSetup takes nothing returns boolean
/* B */ set t_lastmem=t_mem
/* B */ set t_mem=Prev[t_mem]
/* B */ return false
/**/endfunction
    
    private function KTinit takes nothing returns nothing
        set RemoveInstanceCond=Condition(function RemoveInstance)
/* B */ set GetDataCond=Condition(function GetDataSetup)
    endfunction
    
    //////////
    // TAZO //
    ////////////////////////////////////////////////////////////////////////////
    // KT2 implementation for higher periods (low frequency).
    globals
        private constant integer TAZO_DATAMEM=8190 // Added for KT2 hook. Don&#039;t change.
    endglobals
    
    globals
        private conditionfunc TAZO_LoadDataCond
        private conditionfunc TAZO_RemoveInstanceCond
        
        private timer   array TAZO_TrigTimer
        private integer array TAZO_Data
// B    private boolexpr array TAZO_Boolexpr
        
        private trigger array TAZO_AvailableTrig
        private integer       TAZO_Max=0
        
        private integer       TAZO_ConstructNext=0
        private trigger array TAZO_ConstructTrig
        private integer array TAZO_ConstructCount
    endglobals
    
    globals//locals
        private integer TAZO_ConKey
    endglobals
    private function TAZO_Constructer takes nothing returns boolean
        set TAZO_ConKey=GetData()
        call TriggerExecute(TAZO_ConstructTrig[TAZO_ConKey])
        set TAZO_ConstructCount[TAZO_ConKey]=TAZO_ConstructCount[TAZO_ConKey]-1
        if TAZO_ConstructCount[TAZO_ConKey]==0 then
            set TAZO_Max=TAZO_Max+1
            set TAZO_AvailableTrig[TAZO_Max]=TAZO_ConstructTrig[TAZO_ConKey]
            set TAZO_TrigTimer[TAZO_ConKey]=CreateTimer()
            call TriggerRegisterTimerExpireEvent(TAZO_AvailableTrig[TAZO_Max],TAZO_TrigTimer[TAZO_ConKey])
            return true
        endif
        return false
    endfunction
    
    globals//locals
        private trigger TAZO_DeadTrig
// B    private integer TAZO_DeadCount
    endglobals
    private function TAZO_Recycle takes nothing returns boolean
        set TAZO_DeadTrig=GetTriggeringTrigger()
// B    set TAZO_DeadCount=GetTriggerExecCount(TAZO_DeadTrig)
        call TriggerClearConditions(TAZO_DeadTrig)
// B    call DestroyBoolExpr(TAZO_Boolexpr[TAZO_DeadCount])
/* B */ call PauseTimer(TAZO_TrigTimer[GetTriggerExecCount(TAZO_DeadTrig)])
        set TAZO_Max=TAZO_Max+1
        set TAZO_AvailableTrig[TAZO_Max]=TAZO_DeadTrig
        return false
    endfunction
    
    private function TAZO_LoadData takes nothing returns boolean
        // KT2 Data Hook
/* B */ set t_lastmem=TAZO_DATAMEM
        set Data[TAZO_DATAMEM]=TAZO_Data[GetTriggerExecCount(GetTriggeringTrigger())]
        // End KT2 Data Hook
        return false
    endfunction
    
    private function InitTrigExecCount takes trigger t, integer d returns nothing
        if d&gt;128 then
            call InitTrigExecCount.execute(t,d-128)
            set d=128
        endif
        loop
            exitwhen d==0
            set d=d-1
            call TriggerExecute(t)
        endloop
    endfunction
    
    globals//locals
        private integer TAZO_AddKey
        private trigger TAZO_AddTrigger
/* B */ private boolexpr TAZO_AddBoolexpr
    endglobals
    public function TAZOadd takes code func, integer data, real period returns nothing
        if TAZO_Max==0 then
            // Failsafe.
            set TAZO_ConstructNext=TAZO_ConstructNext+1
            set TAZO_AddTrigger=CreateTrigger()
            set TAZO_AddKey=TAZO_ConstructNext
            call InitTrigExecCount.execute(TAZO_AddTrigger,TAZO_AddKey)
            set TAZO_TrigTimer[TAZO_AddKey]=CreateTimer()
            call TriggerRegisterTimerExpireEvent(TAZO_AddTrigger,TAZO_TrigTimer[TAZO_AddKey])
        else
            set TAZO_AddTrigger=TAZO_AvailableTrig[TAZO_Max]
            set TAZO_AddKey=GetTriggerExecCount(TAZO_AddTrigger)
            set TAZO_Max=TAZO_Max-1
        endif
        set TAZO_Data[TAZO_AddKey]=data
/* B */ set TAZO_AddBoolexpr=LoadBooleanExprHandle(table,&#039;SLOW&#039;,GetHandleId(Condition(func)))
/* B */ if TAZO_AddBoolexpr==null then
/* B */     set TAZO_AddBoolexpr=Or(TAZO_LoadDataCond,And(Condition(func),TAZO_RemoveInstanceCond))
/* B */     call SaveBooleanExprHandle(table,&#039;SLOW&#039;,GetHandleId(Condition(func)),TAZO_AddBoolexpr)
/* B */ endif
// B    call TriggerAddCondition(TAZO_AddTrigger,TAZO_LoadDataCond)
/* B */ call TriggerAddCondition(TAZO_AddTrigger,TAZO_AddBoolexpr)
        call TimerStart(TAZO_TrigTimer[TAZO_AddKey],period,true,null)
        if TAZO_Max&lt;10 then
            set TAZO_ConstructNext=TAZO_ConstructNext+1
            set TAZO_ConstructTrig[TAZO_ConstructNext]=CreateTrigger()
            set TAZO_ConstructCount[TAZO_ConstructNext]=TAZO_ConstructNext
            call KTadd(function TAZO_Constructer,TAZO_ConstructNext,TAZO_CONSTRUCT_PERIOD)
        endif
    endfunction
    
    private function TAZOinit takes nothing returns nothing
        set TAZO_LoadDataCond=Condition(function TAZO_LoadData)
        set TAZO_RemoveInstanceCond=Condition(function TAZO_Recycle)
        
// B    set Next[TAZO_DATAMEM]=TAZO_DATAMEM
// B    set Prev[TAZO_DATAMEM]=TAZO_DATAMEM
        
        loop
            exitwhen TAZO_Max==TAZO_PRECACHE
            set TAZO_ConstructNext=TAZO_ConstructNext+1 // The index.
            set TAZO_Max=TAZO_Max+1 // Will be the same in the initialiser as ConstructNext.
            set TAZO_AvailableTrig[TAZO_Max]=CreateTrigger()
            call InitTrigExecCount.execute(TAZO_AvailableTrig[TAZO_Max],TAZO_ConstructNext)
            set TAZO_TrigTimer[TAZO_ConstructNext]=CreateTimer()
            call TriggerRegisterTimerExpireEvent(TAZO_AvailableTrig[TAZO_Max],TAZO_TrigTimer[TAZO_ConstructNext])
        endloop
    endfunction
    
    ///////////////
    // Interface //
    ////////////////////////////////////////////////////////////////////////////
    // Stitches it all together neatly.
    public function Add takes code func, integer data, real period returns nothing
        if period&lt;PERIODTHRESHOLD then
            call KTadd(func,data,period)
        else
            call TAZOadd(func,data,period)
        endif
    endfunction
    
    private module InitModule
        private static method onInit takes nothing returns nothing
            call KTinit()
            call TAZOinit()
        endmethod
    endmodule
    
    private struct InitStruct extends array
        implement InitModule
    endstruct
endlibrary

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//    End of Key Timers 2
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

Jesus4Lyf

Good Idea™
Reaction score
397
I already tried that method a year ago. It's slower, if I recall correctly. :)
There is only one valid optimisation to KT2 as far as I know, and I haven't bothered to implement it. That is to reduce the GetData function to 2 lines by changing the operation of the linked list. There is one other optimisation which is to split up the GetData method into separate, inlining functions, but I dare say that's ridiculous. :p

Edit: By the way, one key feature of KT2 is it is backwards compatible through patch 1.23, which yours is not. Not that that's a massive issue, but I'd like to keep it that way, ideally. :)
 

Bribe

vJass errors are legion
Reaction score
67
Did you factor in the KT_GetData() call in the functions you benchmarked? Not requiring a user to call it once and only once is a nice plus, especially since KT_GetData() inlines the way I have it.

Having an option for users who still play around with 1.23 to use the current KeyTimers is fine, but the vast majority of users have 1.24 by now, since Battle.net requires it. That way, most of us will be recycling already-existing boolean expressions instead of creating thousands of handles throughout the course of the game...

The neat thing is that someone can use either version -- like two "flavors" (such as TimerUtils uses) -- without any U.I. changes.
 

Jesus4Lyf

Good Idea™
Reaction score
397
Did you factor in the KT_GetData() call in the functions you benchmarked?
Yes. Did you factor in the extra boolexpr evaluation in yours?
Not requiring a user to call it once and only once is a nice plus
Kind of. It doesn't -really- make a difference, especially if you wanted to keep your code backwards compatible.
KT_GetData() inlines the way I have it.
That's irrelevant, because you have an extra boolexpr evaluation. Which, off memory, is slower than a function call.
Having an option for users who still play around with 1.23 to use the current KeyTimers is fine, but the vast majority of users have 1.24 by now, since Battle.net requires it.
Of course, it is no longer a priority to keep KT2 backwards compatible to before 1.23. But at the moment, it happens to be. :)
That way, most of us will be recycling already-existing boolean expressions instead of creating thousands of handles throughout the course of the game...
Is there a benefit to this? -That's- the question. It's an optimisation on only the KT_Add call, that's for sure. And if you do this, you might as well modify the interface completely: KT_Create(function myFunc) --> boolexpr, KT_Add(KTboolexpr, timeout, data).
The neat thing is that someone can use either version -- like two "flavors" (such as TimerUtils uses) -- without any U.I. changes.
Sure. But the better version is still the one in the first post of this thread... until we can show otherwise. :thup:
And of course, if we -can- show otherwise, the first version shall be updated, and you get some sort of credit note. :)
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Staff online

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top