HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

PeriodicLoop

08-16-2011, 04:26 PM#1
BBQ
Introduction
PeriodicLoop is written in Zinc and requires the Jass NewGen Pack along with the latest version of JassHelper.

The main purpose of this library 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. Furthermore, all of the iteration is done by using a single timer and a trigger - nothing more, nothing less.

The linked list is built with the help of a sentinel value (the null struct), so the insertion and the removal of instances are pretty fast as well.

The user is also provided with internal wrappers that should be used when dealing with static functions. Those wrappers are implemented with the help of a dummy struct and function pointers.
CreditsThe script
Collapse PeriodicLoop:
//! zinc
library PeriodicLoop requires optional xebasic {
/**
 *      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 that = 0, globalCount = 0; // The former will be used to temporarily store a node, while the
    }                                      // latter will be used to keep count of the running instances.

    public module PeriodicLoop
    {
        private // Module-private members.
        {
            thistype next, previous; // These, along with the null struct, will help us build the doubly linked list.
            boolean alreadyInserted, pendingRemoval; // Self-explanatory, I hope.
            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
        {
            thistype this = thistype(0).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 (integer(this) != 0) // Traverse the list.
            {
                if (this.pendingRemoval) // See if the current node should be removed.
                {
                    this.pendingRemoval = false;

                    // Remove the node.
                    this.previous.next = this.next;
                    this.next.previous = this.previous;
                    
                    if (this.alreadyInserted) // See if the node should be reinserted.
                    {
                        that = integer(this.next); // Temporarily store the next node.
                        
                        // Reinsert it at the end of the list.
                        thistype(0).previous.next = this;
                        this.previous = thistype(0).previous;
                        thistype(0).previous = this;
                        this.next = thistype(0);
                        
                        if (thistype(that) == thistype(0)) // If the next node was in fact null,
                            this = thistype(0).previous;   // then go back to the previous one.
                        else 
                            this = thistype(that); // Otherwise, go to the next node normally.
                    }
                    else
                    {
                        globalCount -= 1; // Decrement the global count.
                        
                        if (globalCount == 0) // Check whether the timer should keep running.
                            PauseTimer(mainTimer);
                            
                        if (thistype(0).next == thistype(0)) // Check if the iterator should be detached from the trigger.
                            TriggerRemoveCondition(mainTrigger, thistype.instanceIterator);

                        this = this.next; // Go to the next node.
                    }
                }
                else
                {
                    this.onPeriodicLoop(); // Call the .onPeriodicLoop() method.
                    this = this.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 (!this.alreadyInserted && integer(this) > 0) // Make sure the method was called on a valid node.
            {
                this.alreadyInserted = true;
                
                if (!this.pendingRemoval) // First, see if the node was flagged for removal.
                {
                    // And if it wasn't, insert it at the end of the linked list.
                    thistype(0).previous.next = this;
                    this.previous = thistype(0).previous;
                    thistype(0).previous = this;
                    this.next = thistype(0);
                
                    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); });

                    if (this.previous == thistype(0)) // Check if the iterator should be (re)attached to the trigger.
                        thistype.instanceIterator = TriggerAddCondition(mainTrigger, Condition(static method thistype.iterator));
                }
            }
        }

        method stopPeriodicLoop()
        {
            if (this.alreadyInserted && integer(this) > 0) // Make sure the method was called on a valid node.
            {
                this.pendingRemoval = true; // Flag this node so that it would be removed during the next loop.
                this.alreadyInserted = false;
            }
        }
    }

    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

Usage examples
Expand Example #1:

Expand Example #2:
08-16-2011, 08:16 PM#2
Anitarf
There is a problem here. What happens if you call .stopLooping() and then .startLooping() from the onLoop method? The struct instance will be appended at the end of the list while still being the current loop instance, potentially causing a bunch of instances to miss an update. I can think of no simple way to work around this, there is a reason Vexorian used boolean returns instead.
08-16-2011, 10:22 PM#3
BBQ
Quote:
Originally Posted by Anitarf
There is a problem here. What happens if you call .stopLooping() and then .startLooping() from the onLoop method? The struct instance will be appended at the end of the list while still being the current loop instance, potentially causing a bunch of instances to miss an update. I can think of no simple way to work around this, there is a reason Vexorian used boolean returns instead.
Yes, I am aware of that (and had actually put a warning in the documentation but decided to remove it before submitting), however, from my point of view, it doesn't make sense to follow up .stopLooping() with .startLooping(). If you needed to do that, you shouldn't have paused the instance in the first place. I guess I should put that warning back up.

Anyway, this approach is quite faster (and arguably more user-friendly) than the one in Vexorian's TimedLoop, so I guess it's all up to you.
08-16-2011, 11:33 PM#4
Anitarf
Quote:
Originally Posted by BBQ
Yes, I am aware of that (and had actually put a warning in the documentation but decided to remove before submitting it), however, from my point of view, it doesn't make sense to follow up .stopLooping() with .startLooping().
Ah, but that is not what you do. What you do is destroy an instance (which involves stopping it first) and then (directly or indirectly through events) create a new one which reuses the index of the destroyed one (and is then started). This kind of thing will happen and the system should be able to handle it without error as the users can't really be expected to ensure that no complicated sequence of events can result in a struct instance being created immediately after another one was destroyed.

The simplest way of fixing this I can think of is to flag instances that are stopped and only remove them from the list when you're looping through it. Then you have two options for what to do with instances that are re-started before the loop gets to remove them:
  • Simply unset the flag so that the loop won't remove them any more, however with this solution the instances are no longer guaranteed to be listed in the same order they were added to the list.
  • Set a second flag that tells the loop to not only remove the instance from the list while looping through it, but also append it at the end of the list.
This would only add an if statement with a boolean array read to your loop. I don't think that's something to worry about.
08-17-2011, 01:21 AM#5
BBQ
I realize that, but it's still the same instance, so there's still no reason to pause and re-start it... or at least I think so.

But anyway, if I were to eliminate that tiny gimmick, the best way would be to simply resort to boolean returns, which simply wouldn't be as nice as it currently is. However, if you mark with such small flaws as unusable, then I guess it can't be helped. Also, I don't know how this module compares to Nestharus'.
08-17-2011, 12:08 PM#6
Anitarf
Quote:
Originally Posted by BBQ
I realize that, but it's still the same instance, so there's still no reason to pause and re-start it... or at least I think so.
Like I said, you're not pausing and restarting the same instance. You're pausing an instance because you destroy it. Then you create a new instance (which is given the index of the last destroyed instance by the allocator) and start that.

I've encountered problems like this when writing my systems before. An earlier version of CineScript which wasn't as robust as the current one would bug out precisely for this reason when I used it in my action map.

Quote:
But anyway, if I were to eliminate that tiny gimmick, the best way would be to simply resort to boolean returns, which simply wouldn't be as nice as it currently is. However, if you mark with such small flaws as unusable, then I guess it can't be helped.
If it's not as nice, how is this the best way then? There are simple ways to eliminate this problem that retain the current API, so it can be helped quite easily.

Quote:
Also, I don't know how this module compares to Nestharus'.
It compares quite favourably, seeing as how its API is not utterly insane.
08-18-2011, 12:03 AM#7
BBQ
Updated. Please let me know if there's anything more to it.

And about the issue we've been discussing, I couldn't think of a sane way to make it so that the instance will be inserted at the end of the list (like it normally would be). Instead, I made it work by not removing the instance at all. Truth to be told, I don't know if the small overhead in the loop is even worth it, but I guess that public resources should be as robust as possible.
08-18-2011, 11:40 AM#8
Anitarf
Quote:
And about the issue we've been discussing, I couldn't think of a sane way to make it so that the instance will be inserted at the end of the list (like it normally would be).
This should do the trick, using existing flags:
Expand Zinc:

Quote:
Originally Posted by BBQ
Updated. Please let me know if there's anything more to it.
I'm not all too fond of the use of trigger conditions there, since a thread crash in one resource using this would cause others that get registered afterwards to stop working as well. Since thread crashes shouldn't occur in a working map, this isn't as serious a problem as the one we were solving up until now, which could happen during normal map operation. Considering that the trigger conditions are not a part of the public API (thus no need for dummy boolean returns in user functions), if the speed gain of using trigger conditions over .execute on a function interface is significant enough I guess having this problem is worth it.

What counts as significant enough, though? Using trigger conditions makes the biggest difference when you have many structs implementing this module, but each one is only running very few (ideally one) instances. This seems unlikely. In practice, a map is unlikely to have more than a few missile/knockback/etc systems that use this and each of those systems will be running many instances. The only way to get many implementations of this module that each only run a few instances would be through individual triggered spells, but the problem here is that in most maps spells have a lot of downtime when no instances are running.

In the end, I don't think the speed gain is significant. However, as long as you add something to detect and report thread crashes in debug mode, I won't insist that you change this.


A feature I want to see is the ability to register single functions. I am considering using this in Cinema Workshop to sync all periodic stuff with the camera movement without having to resort to the static if hacks I'm using now. However, I don't want to have to implement this module in a dummy struct just to get my camera system to work.

I am reminded of Earth-Fury's KeyAction which provided both static events as well as struct event modules in its API. Both are useful and this system should provide both as well. You could do this easily by implementing your own module in a private struct with some wrappers, something like this:
Expand JASS:


I think the names of methods could be improved. Just "Loop" is insufficient as it ignores the whole periodic aspect of it. Vexorian ended up naming his thing TimedLoop, but his earlier experiments included PeriodicLoop which I think is also a good name you could use. Likewise, .start/stopLooping could be replaced by something like .periodicLoopStart/Stop. I think it would be even better to drop the loop from the name entirely, especially if you add static functions support. The loop is not the point, the point is to run code periodically, the loop is just a way of running the same code for multiple instances. PeriodicEvent works much better as a name for a library like this, although considering you're limited to a single period you'd need a more specific name than one that implies you cover all possible periodic events. Alternatively, though, you could call it PeriodicEvent and add support for different periods.


So, in conclusion, these are my two main requirements: static functions support and better names. Although a library specialised in one period is okay (have you considered use xe's period instead of declaring your own, though?), I think supporting various periods is worth looking into since it wouldn't involve any extra cost for users who end up using only one period.
08-18-2011, 03:14 PM#9
BBQ
Quote:
Originally Posted by Anitarf
This should do the trick, using existing flags:
Unfortunately, that wouldn't work. It results in exactly the same issue it had earlier.

Quote:
Originally Posted by Anitarf
Considering that the trigger conditions are not a part of the public API (thus no need for dummy boolean returns in user functions), if the speed gain of using trigger conditions over .execute on a function interface is significant enough I guess having this problem is worth it.
The speed gain is very significant. There is a rather nice gain over using one timer per struct as well (one timer evaluating a trigger with multiple conditions is faster than multiple timers).

Quote:
Originally Posted by Anitarf
In the end, I don't think the speed gain is significant. However, as long as you add something to detect and report thread crashes in debug mode, I won't insist that you change this.
I could make it display a message every 15 or something seconds indicating that the op-limit had not been hit in the process.

Quote:
Originally Posted by Anitarf
A feature I want to see is the ability to register single functions. I am considering using this in Cinema Workshop to sync all periodic stuff with the camera movement without having to resort to the static if hacks I'm using now. However, I don't want to have to implement this module in a dummy struct just to get my camera system to work.

I am reminded of Earth-Fury's KeyAction which provided both static events as well as struct event modules in its API. Both are useful and this system should provide both as well. You could do this easily by implementing your own module in a private struct with some wrappers, something like this:
The thing is, calling .execute() or .evaluate() 32 times per second is very, very costly and could hinder the performance with a relatively small amount of running instances.

Quote:
Originally Posted by Anitarf
I think the names of methods could be improved. Just "Loop" is insufficient as it ignores the whole periodic aspect of it. Vexorian ended up naming his thing TimedLoop, but his earlier experiments included PeriodicLoop which I think is also a good name you could use. Likewise, .start/stopLooping could be replaced by something like .periodicLoopStart/Stop. I think it would be even better to drop the loop from the name entirely, especially if you add static functions support. The loop is not the point, the point is to run code periodically, the loop is just a way of running the same code for multiple instances. PeriodicEvent works much better as a name for a library like this, although considering you're limited to a single period you'd need a more specific name than one that implies you cover all possible periodic events. Alternatively, though, you could call it PeriodicEvent and add support for different periods.
I wholeheartedly agree about the names. I know that calling it LoopModule isn't even technically correct, but I suck very hard as far as names are concerned. I'll try to think of something later on.

Quote:
Originally Posted by Anitarf
(have you considered use xe's period instead of declaring your own, though?)
Yes, I have, but until xe is updated both performance- and feature-wise, I'm afraid I won't use its period.

Quote:
Originally Posted by Anitarf
I think supporting various periods is worth looking into since it wouldn't involve any extra cost for users who end up using only one period.
I really don't see a need to support various periods. There's Key Timers 2 by Jesus4Lyf that does that, but it fails quite miserably when compared to something like this module (or Jesus4Lyf's Timer32) for the high-frequency and something like TimerUtils (especially its "orange" flavor) for the low-frequency stuff.
08-21-2011, 12:26 AM#10
BBQ
Changed the module's name to PeriodicLoop and the methods' names to startPeriodicLoop and stopPeriodicLoop (thanks to Anitarf for the suggestion). Please change the thread's title accordingly.

I am still hesitant about including support for static functions. However, if there's anything else that should be either added or improved, please, do let me know.
08-22-2011, 07:18 AM#11
Bribe
Basing this off of evaluations would eliminate the whole advantage, it'd have to
be a module.

Personally I'd rather code all this for each script, with just one timer per struct.
It avoids a lot of the mess, uses fewer handles and fewer library req's.
08-22-2011, 10:38 AM#12
Anitarf
Quote:
Originally Posted by BBQ
Unfortunately, that wouldn't work. It results in exactly the same issue it had earlier.
Right, I messed up there, you would need to also save the next instance in a temporary variable.
Expand Zinc:

Quote:
The speed gain is very significant. There is a rather nice gain over using one timer per struct as well (one timer evaluating a trigger with multiple conditions is faster than multiple timers).
Quote:
The thing is, calling .execute() or .evaluate() 32 times per second is very, very costly and could hinder the performance with a relatively small amount of running instances.
My benchmarks show that the difference between evaluating 10 triggers with one condition each and one trigger with 10 conditions isn't very dramatic. The latter shaves about 25% of the cost of the former. If calling .evaluate 32 times per second is "very, very costly", then so is what you're doing, it's on the same order of magnitude, those 25% don't suddenly make it cheap.

The truth is, while as single operations .evaluate and .execute are rather costly, we don't use them at nearly the same frequency as, say, global array reads and writes. Those add up. In practice, doing a periodic update on several instances of a knockback or a missile struct makes shaving 25% off a single .evaluate seem entirely insignificant.

Like I already said, that doesn't mean I object to you saving those 25%. It doesn't affect the users in any way, so why not make the system run faster internally? That's fine. But don't spout nonsense about it being a significant gain and definitely don't use such nonsense to justify denying features to the users. If I have a single static function that I want to run periodically, I'd like to be able to do that without having to create a dummy struct to implement your module in so that it can then "loop" through a single static instance. That's just wrong.

Besides, it's not even faster. The local variable declaration, a function call with an argument, two array reads, two variable sets and four variable reads (they sure add up, don't they?) likely cost more than 25% of an .evaluate anyway.
Edit: I realize some of this overhead would be present even if you did direct evaluations for static functions, since you'd still need to loop through a list of them, but that overhead would be smaller per instance, for example you would only need one local declaration for all instances.

Quote:
I could make it display a message every 15 or something seconds indicating that the op-limit had not been hit in the process.
I was thinking more along the lines of something like this:
Expand JASS:

Quote:
Yes, I have, but until xe is updated both performance- and feature-wise, I'm afraid I won't use its period.
What extra features do you think xebasic needs?


Quote:
Originally Posted by Bribe
Basing this off of evaluations would eliminate the whole advantage, it'd have to be a module.
From my perspective, this system has merit not because it's faster, but because it's functional. It allows me to easily sync all my periodic updates, which is important in situations when these updates are interdependent, like the unit and camera movement in Cinema Workshop. Basing this off of evaluations would eliminate none of that.

Quote:
Personally I'd rather code all this for each script, with just one timer per struct.
It avoids a lot of the mess, uses fewer handles and fewer library req's.
I'd agree about the rest, but how is a timer per implementation fewer handles than a trigger condition per implementation?
08-22-2011, 05:46 PM#13
BBQ
Updated.

EDIT: Is the documentation sufficient? Should I provide "usage examples" either within the opening post or embed them into the documentation itself?

Quote:
Originally Posted by Anitarf
Right, I messed up there, you would need to also save the next instance in a temporary variable.
I should have thought of that earlier. Just note that I modified it a bit because your version would perform differently depending on whether the node was on the back of the list or somewhere in-between.

Quote:
Originally Posted by Anitarf
I was thinking more along the lines of something like this:
Okay, that's a much better option. Just note that I won't be able to catch & report thread crashes that happen within the static functions, but those shouldn't be a problem, as .execute() and .evaluate(), unlike normal function calls, start a new instance of the VM (that naturally has its own op-limit).

Quote:
Originally Posted by Anitarf
What extra features do you think xebasic needs?
I was talking about the rest of xe. Do people even use xebasic as a standalone library?

Quote:
Originally Posted by Bribe
Personally I'd rather code all this for each script, with just one timer per struct.
It avoids a lot of the mess, uses fewer handles and fewer library req's.
You could say the same about pretty much any library. And about "avoiding the mess", it's quite the contrary - this module helps you "hide" a lot of ugly code from your structs.
08-23-2011, 10:20 AM#14
Bribe
Quote:
Originally Posted by Anitarf
I'd agree about the rest, but how is a timer per implementation fewer handles than a trigger condition per implementation?

Well there is the option for one timer per struct,

or there is the option (using a timer system like this) to use one trigger and
one timer for the system, but then a triggercondition and a boolexpr for
each struct.

It offers a readability advantage sure, and something like this has been
approved on the other major communities like HiveWorkshop.com
and TheHelper.net. So I don't see a problem that wc3c.net gets their own
version.
08-23-2011, 11:51 PM#15
Anitarf
Quote:
Originally Posted by BBQ
Is the documentation sufficient? Should I provide "usage examples" either within the opening post or embed them into the documentation itself?
A usage example would be fine, doesn't have to be anything special, in fact the simpler the better, a struct that just rotates a unit's facing would be a good example. Maybe it could be in vJass for all the people who aren't familiar with zinc?

As for the documentation, maybe a few things could be reordered a bit, it is very important that the onPeriodicLoop method is declared above the module, so I would mention that in the same place where you first specify that this method must be declared, rather than mentioning this more as an afterthought. The start/stop methods are less important since they're not used as frequently, so I would just mention that using them from above the module is more costly, I wouldn't instruct the users to actually make sure the module is implemented above all such calls, sometimes that's not even possible to do.

Likewise, I wouldn't say that static functions shouldn't be used when working with structs, since there can be valid reasons for using them. For example, registering a single static method which then does a ListModule-based loop would be faster than using your module if enough instances are likely to exist simultaneously since this alternative saves a function call per instance while the extra cost of using a full evaluate is paid just once. I guess you meant to say people shouldn't use this to run individual instances but they can't really do that anyway since no arguments can be passed to the function, so how would it know which instance to run?

These are just minor quibbles, though, the documentation is fine as it is.

Quote:
I should have thought of that earlier. Just note that I modified it a bit because your version would perform differently depending on whether the node was on the back of the list or somewhere in-between.
Good catch, it looks correct now.

Quote:
Okay, that's a much better option. Just note that I won't be able to catch & report thread crashes that happen within the static functions, but those shouldn't be a problem, as .execute() and .evaluate(), unlike normal function calls, start a new instance of the VM (that naturally has its own op-limit).
Doesn't matter, the reason to report such crashes is to help users find the error when one instance causes the whole loop to fail. If the loop isn't affected, then it's not your job to report an error anyway. The debug message should report the name of the implementing struct that crashed, though, like for example Itemdex does it with its debug messages.

I'm not sure that .evaluate protects the starting thread from a crash, though, since the starting thread is still waiting for a boolean return. It doesn't really matter, though, if static functions can cause a loop crash then the error message will clearly inform the user of that as long as you give your dummy struct a good descriptive name.

Also, I'm not sure about the limitations of zinc, but can you use the debug keyword in global declarations as well? You don't need to declare the safety boolean outside debug mode.

Quote:
I was talking about the rest of xe. Do people even use xebasic as a standalone library?
Less than they should. Maybe that's Vex's fault for naming the libraries like he did. Doesn't matter how much he stressed its modularity in the thread, people mostly consider xe as a monolithic system, which is a shame. Declaring your own constant also burdens you with having to come up with a unique name that won't conflict with other systems that also declare their own constant, LOOP_INTERVAL seems far too generic, at the very least it should be PERIODIC_LOOP_INTERVAL and even that sounds like something other people are likely to use. xe's constant doesn't have this issue.

32/s also seems like a rather low frequency to me. I guess speed freaks like to use it so they can then boast about how many instances they can run without lag, but anything below 40/s seems like it would be too choppy to me. Personally, I most often use 50/s for stuff like this. Not that it matters much what the default value is since the constant is configurable, so that's okay. At least you didn't put 32 in the name of the library like some people do.


Quote:
Originally Posted by Bribe
or there is the option (using a timer system like this) to use one trigger and
one timer for the system, but then a triggercondition and a boolexpr for
each struct.
Right, I forgot you get both a boolexpr and a triggercondition. I stand corrected.