• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

LinkedStruct and Looper

Status
Not open for further replies.
So essentially, I've been thinking of writing up on the API and functionality of two simple modules I've made to streamline some systems I've also made, then I got to thinking they might be useful to other people. The purpose of these modules is not to bring any sort of new, groundbreaking functionality at all, it's to simplify some things.

LinkedStruct


JASS:
    module LinkedStruct
        
        private static integer maxInstances = 8190
        private static integer count = 0
        
        private static thistype head  = 0 
        private static thistype last  = 0      
        
        private static boolean array active
        private static boolean array used
        private static integer array next
        private static integer array prev
        
        private static integer lastid = 0
        private static thistype array recycled
        private static integer recycledCount = 0
        
        private static timer          deallocTimer    = CreateTimer()
        private static boolean        deallocRunning  = false
        private static thistype array deallocInstances
        private static integer        deallocCount    = 0
        
        public method getMaxInstances takes nothing returns integer
            return maxInstances
        endmethod
        
        public method setMaxInstances takes integer max returns nothing
            set maxInstances = max
        endmethod
        
        method exists takes nothing returns boolean
            return used[this]
        endmethod
        
        static method getCount takes nothing returns integer
            return count
        endmethod
        
        static method getHead takes nothing returns thistype
            return head
        endmethod
        
        static method getFirst takes nothing returns thistype
            return head.getNext()
        endmethod
        
        static method getLast takes nothing returns thistype
            return last
        endmethod
        
        method getNext takes nothing returns thistype
            local thistype inst = this
            loop
                set inst = next[inst]
                if active[inst] then
                    return inst
                else
                    if inst == head then
                        return inst
                    endif
                endif
            endloop
            return thistype(0)
        endmethod
        
        method getPrev takes nothing returns thistype
            local thistype inst = this
            loop
                // Looping until we find the first list member
                // that is labeled active, or we'll hit the head member
                // and promptly just return it
                set inst = prev[inst]
                if active[inst] then
                    return inst
                else
                    if inst == head then
                        return inst
                    endif
                endif
            endloop
            return thistype(0)
        endmethod
        
        private static method getFreeIndex takes nothing returns integer
            if count == maxInstances then
                return 0
            endif
            
            if recycledCount == 0 then
                set lastid = lastid + 1
                set used[lastid] = true
                return thistype(lastid)
            else
                set recycledCount = recycledCount - 1
                set used[recycled[recycledCount]] = true
                return recycled[recycledCount]
            endif
        endmethod
    
        public static method alloc takes nothing returns thistype
            // First, let's secure an unique used index
            local integer new = getFreeIndex()
            
            // Then let's check if we're actually given one
            if new != 0 then
                // Increase the counter
                set count = count + 1
                
                // Let's add the new element to the right (next) side
                // of the last element in the linked list
                set next[last] = new
                set prev[new ] = last
                
                // Since this is a doubly linked list, we'll have the
                // new last element point to the head now, and vice versa
                set last = new
                set next[last] = head
                set prev[head] = last
                
                // When an instance member is set to active, it won't be
                // skipped over during the list iteration
                set active[new] = true
            endif
            return thistype(new)
        endmethod
        
        public method dealloc takes nothing returns nothing
            // We are only deallocating the instances that are labeled as 
            // used in order to
            if used[this] then
                set active[this] = false
                set used[this] = false
                
                set next[prev[this]] = next[this]
                set prev[next[this]] = prev[this]
                
                if this == last then
                    set last = prev[this]
                endif
                
                set prev[this] = 0
                set next[this] = 0

                set recycled[recycledCount] = this
                set recycledCount = recycledCount + 1
            endif
        endmethod
        
        private static method timedDealloc takes nothing returns nothing
            local integer i = 0
            loop
                exitwhen i >= deallocCount
                call deallocInstances[i].dealloc()
                set i = i + 1
            endloop
            set deallocCount = 0
            set deallocRunning = false
        endmethod
        
        public method collect takes nothing returns nothing
            // It is no longer active, but the index is not used until it's deallocated
            if active[this] then
                set active[this] = false
                
                // Add it to the dealloc list
                set deallocInstances[deallocCount] = this
                set deallocCount = deallocCount + 1
                
                // If the deallocation system is not already running
                if not deallocRunning then
                    call TimerStart(deallocTimer, 0, false, function thistype.timedDealloc)
                    set deallocRunning = true
                endif
                
                // Reduce count
                set count = count - 1
            endif
        endmethod

First we have LinkedStruct. This library lets you call .alloc(), .collect() and .dealloc() to allocate, deallocate in the next frame, and deallocate immediately.
It has a built in linked list that contains instance members.


Why would I do this, you ask? Well, my idea is as follows: I want to be able to iterate through the list of instances as easily, consistently and safely as possible. Therefore, to do so I can just do this:

JASS:
thistype instance = thistype.getHead()
loop
    set instance = instance.getNext()
    exitwhen instance == 0
    //Do whatever with instance
endloop

This is all nice and all, but what's the .collect() for? Well, .collect() deallocates the instance we collected with a 0.00 timer delay. This means that during the execution of our iteration the underlying linked list will not change, aside from maybe having new members attached at the end of it, in which case they'll just be iterated over as well. In case we destroy some members the list will not act funky in any way, it'll just be circumvented around them.



Looper


JASS:
    module Looper
        implement optional LinkedStruct
        
        private static timer   looperTimer   = CreateTimer()
        private static boolean looperRunning = false
        private static real    looperDefault = -666
        
        public static method startLooper takes nothing returns nothing
            if not looperRunning and getCount() > 0 then
                call TimerStart(looperTimer, looperTimeout, true, function thistype.looper)
                set looperRunning = true
            endif
        endmethod

        public static method stopLooper takes nothing returns nothing
            if looperRunning then
                call PauseTimer(looperTimer)
                set looperRunning = false
            endif
        endmethod

        public static method setLooperTimeout takes real timeout returns nothing
            set looperTimeout = timeout
            if getCount() > 0 then
                if looperRunning then
                    call stopLooper()
                    call startLooper()
                else
                    call startLooper()
                endif
            endif
        endmethod
        
        public static method looperAutoSetup takes nothing returns nothing
            local integer c = getCount()
            if looperDefault == -666 then
                set looperDefault = looperTimeout
            elseif c == 0 and not looperRunning then 
                call stopLooper()
            elseif c < 10 and looperTimeout != looperDefault then
                call setLooperTimeout(looperDefault)
            elseif (c / 10) * 10 == c then
                call setLooperTimeout(looperDefault * (1+(c/10)))
            elseif c == 0 then
                call stopLooper()
            endif
            call startLooper()
        endmethod
        
    endmodule


And now about Looper: Whenever you have a system that requires a single big timer to run it, you can use something like this. Essentially it includes methods to automatically manage whether the timer runs or not. It uses LinkedStruct to determine the number of current instances of the struct, and then modifies the timer timeout according to that. How is it used, though? Well...

JASS:
public static method create takes nothing returns thistype
    local thistype instance = thistype.alloc()
    //stuff
    call looperAutoSetup()
    return instance
endmethod

JASS:
public method onDestroy takes nothing returns nothing
    call looperAutoSetup()
endmethod

However, in order for Looper to work, you need to define a static real variable called looperTimeout and set its value to your preferred timeout. However, for every 10 instances, this number will be increased by the base value. Which means if you set your timer to 0.01 timeout for a knockback system, once there are 50 instances, it'll run at 0.05, and automatically update once the number of instances goes down. You can, of course, do this manually every time you create/destroy instances, this just does that automatically.
 
Level 7
Joined
Oct 19, 2015
Messages
286
There are better solutions than delaying deallocation with timers. In fact, using timers is probably the worst solution.

return head.getNext() Due to the order of your methods, you're using a TriggerEvaluate here.

elseif c < 10 You shouldn't use unnamed constants, especially if this is supposed to be a public release.

private static integer array next please use struct syntax for better readability: private thistype next

local thistype inst = this Why? Just use this directly.

private static thistype last Why? Just use head.prev.
 
There are better solutions than delaying deallocation with timers. In fact, using timers is probably the worst solution.

Some form of TriggerExecute/Evaluate or ExecuteFunc?

return head.getNext() Due to the order of your methods, you're using a TriggerEvaluate here.

Good catch, gotta change the order.

elseif c < 10 You shouldn't use unnamed constants, especially if this is supposed to be a public release.

You're right, this is work in progress, but there's no reason to make it non-customizable.

private static integer array next please use struct syntax for better readability: private thistype next

But the struct is referring to the list, wouldn't your approach require a Node struct?

local thistype inst = this Why? Just use this directly.
Because of the loop bellow. It starts iteration from this.

private static thistype last Why? Just use head.prev.
[/quote]

Yup, I actually thought of removing that, then forgot.

Thanks.
 
Level 7
Joined
Oct 19, 2015
Messages
286
Some form of TriggerExecute/Evaluate or ExecuteFunc?
No, I meant not using a delay at all.

But the struct is referring to the list, wouldn't your approach require a Node struct?
Since you need one node struct instance per one implementing struct instance, they can simply be the same struct. You're already doing it that way, it's just different syntax. Otherwise, using the struct instance as an index in a static array is the exact same thing as using a non-static struct member, so next[this] and this.next are the same thing, the latter just looks nicer.


Because of the loop bellow. It starts iteration from this.
Yes, but since you don't need the starting value of this inside the loop or afterwards, there's no problem if you overwrite it.

Edit: One more thing: implement optional LinkedStruct It is not optional.
 
Level 13
Joined
Nov 7, 2014
Messages
571
JASS:
struct Foo
    implement LinkedStruct
endstruct

The above amounts to a "global" linked list of type Foo right?
But that's rarely useful, in my experience at least.

It seems that the common case is for an instance to have a list of some other type, say Bar:
JASS:
struct Foo
    Bar bars // each foo instance has it's own list of bars
endstruct

Also, the "circularity" of the list and a "reference" to the last element are things that don't
come in handy often, I think. Usually you only want to add/remove some nodes to/from the list
and then iterate it forward.


It's fine that you've added comments but some of them are not exactly helpful:
// We are only deallocating the instances that are labeled as
// used in order to

// Increase the counter
set count = count + 1
 
Oh yeah, it used to be optional, it isn't anymore.
Thanks for the response again, I'll see to fix this throughout. I just seem to have left a lot of redundancy here since I've changed the script a lot. Also gotta work on making it more vjass like.

The above amounts to a "global" linked list of type Foo right?
But that's rarely useful, in my experience at least.

It seems that the common case is for an instance to have a list of some other type, say Bar:

Yes, but the way to access it can either be public or create through .getHead() and .getNext() methods, pretty much.

No, I meant not using a delay at all.

.dealloc() will circumvent the list as well, I am just afraid that taking members out in a weird order might give weird iteration results. What's your take on it? Should it cause problems?
 
Level 7
Joined
Oct 19, 2015
Messages
286
.dealloc() will circumvent the list as well, I am just afraid that taking members out in a weird order might give weird iteration results. What's your take on it? Should it cause problems?
Not if you code it correctly.

The only time you really need a static list of all instances of a struct is when you're doing periodic updates, so splitting it into two libraries doesn't really achieve anything meaningful even if it may seem like the right thing to do since it makes the code more modular.

So I'd merge it into one library. Except I wouldn't, because that's already been done, so I'd just use the existing library.

Okay, not quite, I did rewrite parts of it since I figured out a simpler way to avoid problems when instances get removed from the list mid-update:

JASS:
//! zinc
library PeriodicLoop requires optional xebasic {
// This library was originally writen by BBQ.
// This version has been optimized by Anitarf.
/**
 *      The main purpose of PeriodicLoop is to provide the user with a module that implements a fully optimized
 *   periodic loop for a struct. Instances that are added to the loop are stored in a doubly linked list, which improves
 *   the loop's efficiency and allows the removal to be just as easy as the insertion. The user is also provided with
 *   internal wrappers that are meant to be used when dealing with static functions, as opposed to the regular, non-static
 *   methods. These wrappers are implemented with the help of a dummy struct and function pointers.
 *
 *      Note that this script was written solely for high-frequency loops, and is by default configured to loop 40 times per
 *   second. You can change this interval by editing PERIODIC_LOOP_INTERVAL constant found right below the documentation.
 *   That constant is also public, and as such should be used with all "over time" effects in order to ensure consistency,
 *   accuracy and portability. Neither multiple intervals nor dynamic adjustments in the interval are supported.
 *
 *      If the xebasic library is present within the map, then the XE_ANIMATION_PERIOD constant will be used instead of
 *   PERIODIC_LOOP_INTERVAL. Please, do not set these constants to anything higher than 0.05, as that would result in lots
 *   of stuff being inaccurate and bumpy. The most commonly used values are 0.025 and 0.03125.
 *   
 *   Now, let's elaborate on the implementation of the script. As I mentioned earlier, you can utilize two "types" of API:
 *
 *      - For the regular, module-based API, the following things need to be done:
 *   
 *   1) Implement the "PeriodicLoop" module within your struct.
 *   2) Declare a non-static method named "onPeriodicLoop". That particular method will be called every interval
 *      as long as there are nodes on the looping list.
 *   3) When you're willing to add a node, simply call structInstance.startPeriodicLoop(), and when you're willing
 *      to remove it, call structInstance.stopPeriodicLoop().
 *   4) Make sure that the module is implemented above all .startPeriodicLoop() calls and below the 
 *      "onPeriodicLoop" method in order to avoid unnecessary trigger evaluations. This is very important.
 *   5) Do not leave destroyed instances in the loop - always call .stopPeriodicLoop() before calling .destroy() or
 *      .deallocate().
 *
 *      - Registering static functions is even easier. The API is the following:
 *      
 *   1) RegisterPeriodicResponse takes response whichResponse returns nothing
 *   2) UnregisterPeriodicResponse takes response whichResponse returns nothing
 *   
 *      In the above two functions, "response" is a function pointer (you can see its definition below), so "whichResponse"
 *   is the name of a function or a static method that takes and returns nothing. Akin to the "onPeriodicLoop" method, the
 *   passed response will be called every interval until UnregisterPeriodicResponse(response) is called.
 */
    public constant real PERIODIC_LOOP_INTERVAL = 1.0/40.0;
    
    type response extends function();

    private
    {
        trigger mainTrigger = CreateTrigger(); // The trigger onto which the iterators will be attached.
        timer mainTimer = CreateTimer(); // This timer will be used to periodically evaluate the above trigger.
        integer globalCount = 0; // Will be used to keep count of the running instances.
        
        function add (code c)-> triggercondition
        {
            globalCount += 1; // Increment the global count.
            if (globalCount == 1) // Check if the timer should be started (or resumed).
                static if (LIBRARY_xebasic)
                    TimerStart(mainTimer, XE_ANIMATION_PERIOD, true, static method() { TriggerEvaluate(mainTrigger); });
                else
                    TimerStart(mainTimer, PERIODIC_LOOP_INTERVAL, true, static method() { TriggerEvaluate(mainTrigger); });
            return TriggerAddCondition(mainTrigger, Condition( c ));
        }

        function remove (triggercondition c)
        {
            globalCount -= 1; // Decrement the global count.
            if (globalCount == 0) // Check whether the timer should keep running.
                PauseTimer(mainTimer);
            TriggerRemoveCondition( mainTrigger, c );
        }
        
    }

    public module PeriodicLoop
    {
        private // Module-private members.
        {
            static thistype root = thistype(0);
            static thistype updated; 
            thistype next, prev;

            static triggercondition instanceIterator = null; // We will use this variable to store the struct's iterator.
            static boolean threadCrashed = false; // In debug mode, this boolean will help us catch and report thread crashes.
        }
        
        private static method iterator() -> boolean
        {
            updated = root.next; // Get the first node.
            
            debug
            {
                if (thistype.threadCrashed) // Warn the user if the thread had crashed during the last loop.
                {
                    BJDebugMsg("PeriodicLoop warning: The thread had crashed during the last loop!\n" +
                    "Make sure you are not performing very intensive operations or using uninitialized variables!\n" +
                    "The name of the iterator in which the thread crashed is \"" + thistype.iterator.name + "\"");
                }
                thistype.threadCrashed = true;
            }
            
            while (updated != root) // Traverse the list.
            {
                    updated.onPeriodicLoop(); // Call the .onPeriodicLoop() method.
                    updated = updated.next; // Go to the next node.
            }

            debug thistype.threadCrashed = false; // If we made it here, then the thread hasn't crashed.

            return false;
        }

        method startPeriodicLoop() 
        {
            if (integer(this) > 0 && this.next==root && root.prev!=this) // Make sure the method was called on a valid node.
            {
                // Insert it at the end of the linked list.
                root.prev.next = this;
                this.prev = root.prev;
                root.prev = this;
                this.next = root;

                if (this.prev == root) // Check if the iterator should be (re)attached to the trigger.
                    thistype.instanceIterator = add( static method thistype.iterator );
            }
        }

        method stopPeriodicLoop()
        {
            if ( integer(this) > 0 &&(this.next!=root || root.prev==this) ) // Make sure the method was called on a valid node.
            {
                if (this==updated) // Safety check if an instance gets removed during a loop.
                    updated = updated.prev;
                // Remove the node.
                this.prev.next = this.next;
                this.next.prev = this.prev;
                this.next = root;
                this.prev = root;

                if (root.next == root) // Check if the iterator should be detached from the trigger.
                    remove( thistype.instanceIterator );
            }
        }
    }

    private struct staticFunctionHelper[] // This is just a dummy struct.
    {
        method onPeriodicLoop()
        {
            response(integer(this)).evaluate();
        }
        module PeriodicLoop;
    }
    
    public // Below is the API for dealing with static responses.
    {
        function RegisterPeriodicResponse(response whichResponse)
        {
            staticFunctionHelper(integer(whichResponse)).startPeriodicLoop();
        }
        
        function UnregisterPeriodicResponse(response whichResponse)
        {
            staticFunctionHelper(integer(whichResponse)).stopPeriodicLoop();
        }
    }
}
//! endzinc
 
Level 13
Joined
Nov 7, 2014
Messages
571
The main purpose of PeriodicLoop is to provide the user with a module that implements a fully optimized periodic loop for a struct.

JASS:
method startPeriodicLoop()
{
    if (integer(this) > 0 && this.next==root && root.prev!=this) // Make sure the method was called on a valid node.
    {
        // Insert it at the end of the linked list.
        root.prev.next = this;
        this.prev = root.prev;
        root.prev = this;
        this.next = root;

        if (this.prev == root) // Check if the iterator should be (re)attached to the trigger.
            thistype.instanceIterator = add( static method thistype.iterator );
    }
}


trigger mainTrigger = CreateTrigger(); // The trigger onto which the iterators will be attached.
timer mainTimer = CreateTimer(); // This timer will be used to periodically evaluate the above trigger.
integer globalCount = 0; // Will be used to keep count of the running instances.

function add (code c)-> triggercondition
{
    globalCount += 1; // Increment the global count.
    if (globalCount == 1) // Check if the timer should be started (or resumed).
        static if (LIBRARY_xebasic)
            TimerStart(mainTimer, XE_ANIMATION_PERIOD, true, static method() { TriggerEvaluate(mainTrigger); });
        else
            TimerStart(mainTimer, PERIODIC_LOOP_INTERVAL, true, static method() { TriggerEvaluate(mainTrigger); });
    return TriggerAddCondition(mainTrigger, Condition( c ));
}


private static method iterator() -> boolean
{
    updated = root.next; // Get the first node.

    debug
    {
        if (thistype.threadCrashed) // Warn the user if the thread had crashed during the last loop.
        {
            BJDebugMsg("PeriodicLoop warning: The thread had crashed during the last loop!\n" +
            "Make sure you are not performing very intensive operations or using uninitialized variables!\n" +
            "The name of the iterator in which the thread crashed is \"" + thistype.iterator.name + "\"");
        }
        thistype.threadCrashed = true;
    }

    while (updated != root) // Traverse the list.
    {
            updated.onPeriodicLoop(); // Call the .onPeriodicLoop() method.
            updated = updated.next; // Go to the next node.
    }

    debug thistype.threadCrashed = false; // If we made it here, then the thread hasn't crashed.

    return false;
}

Well, it seems that instead of the startPeriodicLoop method calling a function (add) that starts a timer that calls a function(an anonymous) that trigger evaluates/calls a function (10-15x slower than "call function()")
that loops over all instances and calls a method on each (instance.onPeriodicLoop()),

you could make the startPeriodicLoop start a timer that calls a function that loops over all the instances and calls a method on each (no TriggerEvaluate),

or even better: write the logic of onPeridicloop directly into the iterating function that gets called by the timer (may not be modular).
 
Level 7
Joined
Oct 19, 2015
Messages
286
you could make the startPeriodicLoop start a timer that calls a function that loops over all the instances and calls a method on each (no TriggerEvaluate),
Except timers don't simply "call" a function, they start a new thread when they expire which is as costly as a TriggerEvaluate, so your solution isn't particularly more efficient. In fact, doing a trigger evaluate on one trigger with more conditions is less costly than doing more trigger evaluates, so even if the current approach has the additional overhead of the static timer expiring it makes up for that by more efficiently processing the modules.

Most importantly, this approach syncs the updates of all the modules which is important when you want, for example, triggered camera movement to be synchronised with triggered unit movement. A timer per module could not achieve this.

or even better: write the logic of onPeridicloop directly into the iterating function that gets called by the timer (may not be modular).
How is it better if it's not modular? Oh, right, it's "faster"...
 
Level 13
Joined
Nov 7, 2014
Messages
571
Right now:
JASS:
every 1/40 (or so) of a second the timer expires and calls its function:
    timer.call(anon_func)
        anon_func calls TriggerEvaluate
            TriggerEvaluate calls iterator
                iterator calls onPeriodicLoop for each instance
                    onPeriodicLoop updates the instance


Could be:
JASS:
every 1/40 (or so) of a second the timer expires and calls its function:
    timer.call(iterator)
        iterator calls onPeriodicLoop for each instance
            onPeriodicLoop updates the instance


Even faster (not modular/"better"):
JASS:
every 1/40 (or so) of a second the timer expires and calls its function:
    timer.call(update)
        update iterates over all the instances and updates them
 
Status
Not open for further replies.
Top